21st.dev 원본
Inverted Cursor · Gamma UI
포인터 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
저장한 HTML에서는 갤러리의 재생 설정이 적용되지 않아요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다. 실행 HTML에는 미리보기를 위한 실행 환경이 들어 있습니다.
baseline-example.tsx
"use client"
import { Cursor } from "@/components/ui/inverted-cursor"
export default function CursorDemo() {
return (
<div className="relative h-96 w-full cursor-none overflow-hidden">
{/* Custom circular inverted color cursor */}
<Cursor />
{/* Main content centered vertically and horizontally */}
<main className="flex h-full items-center justify-center">
<h1 className="text-4xl font-extrabold select-none">Move your mouse</h1>
</main>
</div>
)
}
LICENSE
MIT License
Copyright (c) Gamma UI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
함께 쓰는 파일 12개 보기
upstream/apps/www/content/docs/components/inverted-cursor.mdx
---
title: Inverted Cursor
date: 2025-11-12
description: A smooth, animated custom cursor component with blend mode effects.
author: mazyar
published: true
---
<ComponentPreview
name="inverted-cursor-demo"
title="Inverted Cursor"
description="Smooth following cursor with mix-blend-difference effect for visual contrast."
/>
## Installation
<CodeTabs>
<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
```bash
npx shadcn@latest add @gammaui/inverted-cursor
```
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="inverted-cursor" title="components/gammaui/inverted-cursor.tsx" />
<Step>Update the import paths to match your project setup.</Step>
</Steps>
</TabsContent>
</CodeTabs>
## Usage
### Basic Usage
Add the Cursor component to your root layout or main app component:
```tsx
import { Cursor } from "@/components/gammaui/inverted-cursor"
export default function RootLayout({ children }) {
return (
<html>
<body>
<Cursor />
{children}
</body>
</html>
)
}
```
### Custom Size
```tsx
import { Cursor } from "@/components/gammaui/cursor"
export default function App() {
return (
<>
<Cursor size={80} />
{/* Your content */}
</>
)
}
```
## Props
| Prop | Type | Default | Description |
| ---- | -------- | ------- | -------------------------------- |
| size | `number` | `60` | Diameter of the cursor in pixels |
## Features
- **Smooth Following**: Uses `requestAnimationFrame` for buttery smooth cursor tracking
- **Easing Animation**: Natural easing effect (0.2 lerp factor) for delayed follow
- **Mix Blend Mode**: Uses `mix-blend-difference` for automatic color contrast
- **Visibility Control**: Automatically shows/hides on mouse enter/leave
- **Performance Optimized**: Efficient animation loop with proper cleanup
- **Native Cursor Hidden**: Automatically hides the default system cursor
- **Accessible**: Includes `aria-hidden` for screen readers
## Customization
### Changing the Easing Speed
Modify the lerp factor in the `animate` function (line 25-26):
```tsx
// Faster (0.3 = more responsive)
const deltaX = (targetX - currentX) * 0.3
const deltaY = (targetY - currentY) * 0.3
// Slower (0.1 = more smooth/laggy)
const deltaX = (targetX - currentX) * 0.1
const deltaY = (targetY - currentY) * 0.1
```
### Custom Styling
Override the default styles with Tailwind classes:
```tsx
<div
ref={cursorRef}
className="pointer-events-none fixed z-50 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 blur-sm"
style={{
width: size,
height: size,
opacity: visible ? 1 : 0,
}}
/>
```
### Different Blend Modes
Try different mix-blend modes for various effects:
```tsx
// Default (inverts colors)
className = "... mix-blend-difference"
// Additive lighting effect
className = "... mix-blend-screen"
// Multiply effect
className = "... mix-blend-multiply"
// Exclusion effect
className = "... mix-blend-exclusion"
```
## Examples
### Large Cursor with Blur
```tsx
;<Cursor size={100} />
// In the component, add blur:
className = "... blur-md"
```
### Dual Cursor Effect
```tsx
export default function App() {
return (
<>
<Cursor size={60} /> {/* Outer cursor */}
<Cursor size={20} />{" "}
{/* Inner cursor - will need className modification */}
{/* Your content */}
</>
)
}
```
### Colored Cursor
```tsx
// Modify the component's className
className =
"fixed pointer-events-none rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 opacity-70 z-50"
```
## Important Notes
- **Desktop Only**: This cursor is designed for desktop experiences with mouse input
- **Z-Index**: The cursor uses `z-50` by default. Adjust if you have elements with higher z-index
- **Performance**: Uses `requestAnimationFrame` for optimal performance
- **Cleanup**: Properly restores the native cursor on unmount
- **Single Instance**: Only use one Cursor component per page to avoid conflicts
## Browser Support
Works in all modern browsers that support:
- `mix-blend-difference` CSS property
- `requestAnimationFrame` API
- CSS transforms
## Accessibility
The cursor includes `aria-hidden="true"` to prevent screen readers from announcing it, as it's purely decorative.
upstream/apps/www/registry/example/inverted-cursor-demo.tsx
"use client"
import { Cursor } from "@/registry/gammaui/inverted-cursor"
export default function CursorDemo() {
return (
<div className="relative h-96 w-full cursor-none overflow-hidden">
{/* Custom circular inverted color cursor */}
<Cursor />
{/* Main content centered vertically and horizontally */}
<main className="flex h-full items-center justify-center">
<h1 className="text-4xl font-extrabold select-none">Move your mouse</h1>
</main>
</div>
)
}
author/apps/www/registry/gammaui/inverted-cursor.tsx
"use client"
import React, { useEffect, useRef, useState } from "react"
interface CursorProps {
size?: number
}
export const Cursor: React.FC<CursorProps> = ({ size = 60 }) => {
const cursorRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
// @ts-ignore
const requestRef = useRef<number>()
const previousPos = useRef({ x: -size, y: -size })
const [visible, setVisible] = useState(false)
const [position, setPosition] = useState({ x: -size, y: -size })
const animate = () => {
if (!cursorRef.current) return
const currentX = previousPos.current.x
const currentY = previousPos.current.y
const targetX = position.x - size / 2
const targetY = position.y - size / 2
const deltaX = (targetX - currentX) * 0.2
const deltaY = (targetY - currentY) * 0.2
const newX = currentX + deltaX
const newY = currentY + deltaY
previousPos.current = { x: newX, y: newY }
cursorRef.current.style.transform = `translate(${newX}px, ${newY}px)`
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
const container = containerRef.current
if (!container) return
const handleMouseMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect()
setVisible(true)
setPosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
})
}
const handleMouseEnter = () => {
setVisible(true)
}
const handleMouseLeave = () => {
setVisible(false)
}
container.addEventListener("mousemove", handleMouseMove)
container.addEventListener("mouseenter", handleMouseEnter)
container.addEventListener("mouseleave", handleMouseLeave)
requestRef.current = requestAnimationFrame(animate)
return () => {
container.removeEventListener("mousemove", handleMouseMove)
container.removeEventListener("mouseenter", handleMouseEnter)
container.removeEventListener("mouseleave", handleMouseLeave)
if (requestRef.current) cancelAnimationFrame(requestRef.current)
}
}, [position, size])
return (
<div ref={containerRef} className="absolute inset-0 cursor-none">
<div
ref={cursorRef}
className="pointer-events-none absolute z-50 rounded-full bg-white mix-blend-difference transition-opacity duration-300"
style={{
width: size,
height: size,
opacity: visible ? 1 : 0,
}}
aria-hidden="true"
/>
</div>
)
}
export default Cursor
author/apps/www/app/globals.css
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: Open Sans, sans-serif;
--font-mono: Menlo, monospace;
--font-serif: Georgia, serif;
--radius: 1.3rem;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
--tracking-normal: var(--tracking-normal);
--shadow-2xl: var(--shadow-2xl);
--shadow-xl: var(--shadow-xl);
--shadow-lg: var(--shadow-lg);
--shadow-md: var(--shadow-md);
--shadow: var(--shadow);
--shadow-sm: var(--shadow-sm);
--shadow-xs: var(--shadow-xs);
--shadow-2xs: var(--shadow-2xs);
--spacing: var(--spacing);
--letter-spacing: var(--letter-spacing);
--shadow-offset-y: var(--shadow-offset-y);
--shadow-offset-x: var(--shadow-offset-x);
--shadow-spread: var(--shadow-spread);
--shadow-blur: var(--shadow-blur);
--shadow-opacity: var(--shadow-opacity);
--color-shadow-color: var(--shadow-color);
--color-destructive-foreground: var(--destructive-foreground);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
alternate;
--animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
@keyframes shimmer-slide {
to {
transform: translate(calc(100cqw - 100%), 0);
}
}
@keyframes spin-around {
0% {
transform: translateZ(0) rotate(0);
}
15%,
35% {
transform: translateZ(0) rotate(90deg);
}
65%,
85% {
transform: translateZ(0) rotate(270deg);
}
100% {
transform: translateZ(0) rotate(360deg);
}
}
}
:root {
--header-height: 64px;
--radius: 1.3rem;
--card: oklch(0.9784 0.0011 197.1387);
--card-foreground: oklch(0.1884 0.0128 248.5103);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1884 0.0128 248.5103);
--primary: oklab(67.23% -0.06787 -0.14566);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.1884 0.0128 248.5103);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.9222 0.0013 286.3737);
--muted-foreground: oklch(0.1884 0.0128 248.5103);
--accent: oklch(0.9392 0.0166 250.8453);
--accent-foreground: oklch(0.6723 0.1606 244.9955);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.9317 0.0118 231.6594);
--input: oklch(0.9809 0.0025 228.7836);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.9784 0.0011 197.1387);
--sidebar-foreground: oklch(0.1884 0.0128 248.5103);
--sidebar-primary: oklch(0.6723 0.1606 244.9955);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.9392 0.0166 250.8453);
--sidebar-accent-foreground: oklch(0.6723 0.1606 244.9955);
--sidebar-border: oklch(0.9271 0.0101 238.5177);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.15);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--tracking-normal: 0em;
--background: oklch(1 0 0);
--foreground: oklch(0.1884 0.0128 248.5103);
}
.dark {
--header-height: 64px;
--background: oklch(0 0 0);
--foreground: oklch(0.9328 0.0025 228.7857);
--card: oklch(0.2097 0.008 274.5332);
--card-foreground: oklch(0.8853 0 0);
--popover: oklch(0 0 0);
--popover-foreground: oklch(0.9328 0.0025 228.7857);
--primary: oklch(0.6692 0.1607 245.011);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.9622 0.0035 219.5331);
--secondary-foreground: oklch(0.1884 0.0128 248.5103);
--muted: oklch(0.209 0 0);
--muted-foreground: oklch(0.5637 0.0078 247.9662);
--accent: oklch(0.1928 0.0331 242.5459);
--accent-foreground: oklch(0.6692 0.1607 245.011);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.2674 0.0047 248.0045);
--input: oklch(0.302 0.0288 244.8244);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.2097 0.008 274.5332);
--sidebar-foreground: oklch(0.8853 0 0);
--sidebar-primary: oklch(0.6818 0.1584 243.354);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.1928 0.0331 242.5459);
--sidebar-accent-foreground: oklch(0.6692 0.1607 245.011);
--sidebar-border: oklch(0.3795 0.022 240.5943);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--radius: 1.3rem;
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.25);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
}
}
.no-scrollbar {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.no-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.radial-gradient {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.radial-gradient:hover {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 0.5);
}
.radial-gradient-border-button {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.linear-mask {
mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
-webkit-mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
}
.linear-overlay {
background-image: linear-gradient(
-75deg,
rgba(255, 255, 255, 0.1) calc(var(--x) + 20%),
rgba(255, 255, 255, 0.5) calc(var(--x) + 25%),
rgba(255, 255, 255, 0.1) calc(var(--x) + 100%)
);
mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
-webkit-mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: xor;
padding: 1px;
}
@keyframes move-x {
from {
transform: translateX(var(--move-x-from));
}
to {
transform: translateX(var(--move-x-to));
}
}
.cpu-architecture {
offset-anchor: 10px 0px;
animation: animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
}
.cpu-line-1 {
offset-path: path("M 10 20 h 79.5 q 5 0 5 5 v 30");
animation-duration: 5s;
animation-delay: 1s;
}
.cpu-line-2 {
offset-path: path("M 180 10 h -69.7 q -5 0 -5 5 v 40");
animation-delay: 6s;
animation-duration: 2s;
}
.cpu-line-3 {
offset-path: path("M 130 20 v 21.8 q 0 5 -5 5 h -25");
animation-delay: 4s;
animation-duration: 6s;
}
.cpu-line-4 {
offset-path: path("M 170 80 v -21.8 q 0 -5 -5 -5 h -65");
animation-delay: 3s;
animation-duration: 3s;
}
.cpu-line-5 {
offset-path: path(
"M 135 65 h 15 q 5 0 5 5 v 10 q 0 5 -5 5 h -39.8 q -5 0 -5 -5 v -35"
);
animation-delay: 9s;
animation-duration: 4s;
}
.cpu-line-6 {
offset-path: path("M 94.8 95 v -46");
animation-delay: 3s;
animation-duration: 7s;
}
.cpu-line-7 {
offset-path: path(
"M 88 88 v -15 q 0 -5 -5 -5 h -10 q -5 0 -5 -5 v -5 q 0 -5 5 -5 h 28"
);
animation-delay: 4s;
animation-duration: 4s;
}
.cpu-line-8 {
offset-path: path("M 30 30 h 25 q 5 0 5 5 v 6.5 q 0 5 5 5 h 35");
animation-delay: 3s;
animation-duration: 3s;
}
@keyframes animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
.shadcn {
offset-anchor: 10px 0px;
animation: shadcn-animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
animation-duration: 4s;
animation-delay: 1.5s;
}
.shadcn-line-1 {
offset-path: path("M 35 14 h 25 q 5 0 5 5 v 23 q 0 5 5 5 h 25");
}
.shadcn-line-2 {
offset-path: path("M 30 50 h 65");
}
.shadcn-line-3 {
offset-path: path("M 36.3 87 h 24 q 5 0 5 -5 v -24 q 0 -5 5 -5 h 25");
}
.compBox {
animation: shadncn-animation-path-backwards;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.57, 0.02, 0, 1);
offset-path: path("M 167 36 h -17 q -5 0 -5 5 v 4 q 0 5 -5 5 h -32");
animation-duration: 4s;
opacity: 0;
animation-delay: 5s;
}
@keyframes shadncn-animation-path-backwards {
0% {
offset-distance: 100%;
opacity: 0;
}
90% {
opacity: 1;
offset-distance: 0%;
}
100% {
opacity: 0;
}
}
@keyframes shadcn-animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
@keyframes landing-marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.animate-landing-marquee {
animation: landing-marquee var(--marquee-duration, 45s) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-landing-marquee {
animation: none;
}
}
author/apps/www/package.json
{
"name": "www",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"preinstall": "npx only-allow bun",
"postinstall": "fumadocs-mdx",
"dev": "next dev",
"prebuild": "bun run build:registry",
"build": "next build",
"build:registry": "npx tsx ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache || true",
"shadcn:build": "npx shadcn build registry.json --output ../www/public/r",
"typecheck": "tsc --noEmit",
"start": "next start",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format:fix": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
"check": "bun run lint && bun run typecheck && bun run format:check"
},
"dependencies": {
"@number-flow/react": "^0.5.10",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-three/fiber": "^9.6.1",
"@react-three/postprocessing": "^3.0.4",
"@tabler/icons-react": "^3.35.0",
"@types/react-syntax-highlighter": "^15.5.13",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"front-matter": "^4.0.2",
"fumadocs-core": "^16.2.5",
"fumadocs-mdx": "^13.0.5",
"jotai": "^2.15.1",
"lucide-react": "^0.552.0",
"motion": "^12.23.24",
"next": "15.2.6",
"next-themes": "^0.4.6",
"postprocessing": "^6.39.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-syntax-highlighter": "^16.1.0",
"rehype-pretty-code": "^0.14.1",
"schema-dts": "^1.1.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"three": "^0.185.1",
"ts-morph": "^27.0.2",
"zod": "^4.1.13"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.1",
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@shikijs/transformers": "^3.20.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^20.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/three": "^0.185.0",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"eslint": "^9.39.2",
"eslint-config-next": "^16.0.10",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"lefthook": "^2.0.4",
"prettier": "^3.6.2",
"rimraf": "^6.1.2",
"shiki": "^3.20.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.47.0"
},
"engines": {
"bun": ">=1.1.43"
},
"prettier": {
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"importOrder": [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^@workspace/(.*)$",
"",
"^types$",
"^@/types/(.*)$",
"^@/config/(.*)$",
"^@/lib/(.*)$",
"^@/hooks/(.*)$",
"^@/components/ui/(.*)$",
"^@/components/(.*)$",
"^@/registry/(.*)$",
"^@/styles/(.*)$",
"^@/app/(.*)$",
"^@/www/(.*)$",
"",
"^[./]"
],
"importOrderParserPlugins": [
"typescript",
"jsx",
"decorators-legacy"
],
"plugins": [
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
]
}
}
Usage.tsx실행 안내·자료
// Local host for the unchanged exact baseline demonstration.
import OriginalDemo from "./baseline-example.tsx";
export default function Demo() { return <><OriginalDemo /></>; }
runtime/author-mount.tsx실행 안내·자료
/** Local sandbox host. Ready is a mount observation, never a verification result. */
import React, { Component, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
declare global {
interface Window {
__STYLEGALLERY_PREVIEW__: { id: string; status: string; errors: string[] };
}
}
export function mount(Demo: React.ComponentType, id: string) {
const state = window.__STYLEGALLERY_PREVIEW__ = { id, status: 'loading', errors: [] as string[] };
const send = (message: object) => parent.postMessage({ ...message, id }, '*');
const report = (error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
if (!state.errors.includes(message)) state.errors.push(message);
state.status = 'error';
document.body.dataset.previewStatus = 'error';
send({ type: 'sg-preview-error', message });
};
window.addEventListener('error', (event) => report(event.error ?? event.message));
window.addEventListener('unhandledrejection', (event) => report(event.reason));
window.addEventListener('securitypolicyviolation', (event) => report(`Blocked by preview policy: ${event.violatedDirective}`));
const theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.documentElement.classList.toggle('dark', theme === 'dark');
document.documentElement.dataset.theme = theme;
// Keep original source bytes while ensuring imported attribution/navigation remains local text.
const removeDestinations = () => document.querySelectorAll('a[href]').forEach((link) => {
link.removeAttribute('href');
link.removeAttribute('target');
});
document.addEventListener('click', (event) => {
if ((event.target as Element)?.closest?.('a')) event.preventDefault();
}, true);
document.addEventListener('submit', (event) => event.preventDefault());
new MutationObserver(removeDestinations).observe(document.getElementById('root')!, { childList: true, subtree: true });
const observe = () => {
const root = document.getElementById('root')!;
const elements = Array.from(root.querySelectorAll('*'));
const rect = root.getBoundingClientRect();
const diagnostics = {
textLength: (root.textContent ?? '').trim().length,
elementCount: elements.length,
visibleElementCount: elements.filter((element) => {
const bounds = element.getBoundingClientRect();
const style = getComputedStyle(element);
return bounds.width > 0 && bounds.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity) !== 0;
}).length,
images: [],
canvases: [],
rootRect: { width: rect.width, height: rect.height },
documentWidth: document.documentElement.scrollWidth,
viewportWidth: innerWidth,
};
send({ type: 'sg-preview-observation', diagnostics });
};
class Boundary extends Component<{ children: React.ReactNode }, { error: string | null }> {
state = { error: null as string | null };
static getDerivedStateFromError(error: Error) { return { error: error.message }; }
componentDidCatch(error: Error) { report(error); }
render() { return this.state.error ? <p role="alert">This preview could not render: {this.state.error}</p> : this.props.children; }
}
function Ready() {
useEffect(() => {
const preference = matchMedia('(prefers-reduced-motion: reduce)');
const syncSVG = () => document.querySelectorAll('svg').forEach((svg) => {
if (preference.matches) svg.pauseAnimations?.();
else svg.unpauseAnimations?.();
});
syncSVG();
preference.addEventListener('change', syncSVG);
let second = 0;
let delayed = 0;
const first = requestAnimationFrame(() => { second = requestAnimationFrame(() => {
if (state.status !== 'error') {
state.status = 'mounted';
document.body.dataset.previewStatus = 'mounted';
send({ type: 'sg-preview-ready' });
observe();
delayed = window.setTimeout(observe, 700);
}
}); });
return () => {
cancelAnimationFrame(first);
cancelAnimationFrame(second);
clearTimeout(delayed);
preference.removeEventListener('change', syncSVG);
};
}, []);
return <Demo />;
}
createRoot(document.getElementById('root')!).render(<Boundary><Ready /></Boundary>);
}
runtime/author-styles.css실행 안내·자료
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--background);
--color-card-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--muted);
--color-secondary-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--muted);
--color-accent-foreground: var(--foreground);
--color-popover: var(--background);
--color-popover-foreground: var(--foreground);
--color-destructive: #dc2626;
--color-destructive-foreground: #ffffff;
--color-input: var(--border);
--color-border: var(--border);
--color-ring: var(--foreground);
--radius-lg: 0.5rem;
--radius-md: 0.375rem;
--radius-sm: 0.25rem;
}
:root {
--background: #ffffff;
--border: #d4d4d8;
--foreground: #18181b;
--muted: #f4f4f5;
--muted-foreground: #71717a;
--primary: #18181b;
--primary-foreground: #fafafa;
background: var(--background);
color: var(--foreground);
color-scheme: light;
font-family: Arial, sans-serif;
}
.dark {
--background: #09090b;
--border: #3f3f46;
--foreground: #fafafa;
--muted: #27272a;
--muted-foreground: #a1a1aa;
--primary: #fafafa;
--primary-foreground: #18181b;
color-scheme: dark;
}
body {
margin: 0;
min-height: 100vh;
}
#root {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: center;
min-height: 100vh;
padding: 24px;
width: 100%;
}
#root > * {
max-width: 100%;
}
noscript {
display: block;
padding: 24px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-play-state: paused !important;
}
}
runtime/applied-theme.css실행 안내·자료
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: Open Sans, sans-serif;
--font-mono: Menlo, monospace;
--font-serif: Georgia, serif;
--radius: 1.3rem;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
--tracking-normal: var(--tracking-normal);
--shadow-2xl: var(--shadow-2xl);
--shadow-xl: var(--shadow-xl);
--shadow-lg: var(--shadow-lg);
--shadow-md: var(--shadow-md);
--shadow: var(--shadow);
--shadow-sm: var(--shadow-sm);
--shadow-xs: var(--shadow-xs);
--shadow-2xs: var(--shadow-2xs);
--spacing: var(--spacing);
--letter-spacing: var(--letter-spacing);
--shadow-offset-y: var(--shadow-offset-y);
--shadow-offset-x: var(--shadow-offset-x);
--shadow-spread: var(--shadow-spread);
--shadow-blur: var(--shadow-blur);
--shadow-opacity: var(--shadow-opacity);
--color-shadow-color: var(--shadow-color);
--color-destructive-foreground: var(--destructive-foreground);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
alternate;
--animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
@keyframes shimmer-slide {
to {
transform: translate(calc(100cqw - 100%), 0);
}
}
@keyframes spin-around {
0% {
transform: translateZ(0) rotate(0);
}
15%,
35% {
transform: translateZ(0) rotate(90deg);
}
65%,
85% {
transform: translateZ(0) rotate(270deg);
}
100% {
transform: translateZ(0) rotate(360deg);
}
}
}
:root {
--header-height: 64px;
--radius: 1.3rem;
--card: oklch(0.9784 0.0011 197.1387);
--card-foreground: oklch(0.1884 0.0128 248.5103);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1884 0.0128 248.5103);
--primary: oklab(67.23% -0.06787 -0.14566);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.1884 0.0128 248.5103);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.9222 0.0013 286.3737);
--muted-foreground: oklch(0.1884 0.0128 248.5103);
--accent: oklch(0.9392 0.0166 250.8453);
--accent-foreground: oklch(0.6723 0.1606 244.9955);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.9317 0.0118 231.6594);
--input: oklch(0.9809 0.0025 228.7836);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.9784 0.0011 197.1387);
--sidebar-foreground: oklch(0.1884 0.0128 248.5103);
--sidebar-primary: oklch(0.6723 0.1606 244.9955);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.9392 0.0166 250.8453);
--sidebar-accent-foreground: oklch(0.6723 0.1606 244.9955);
--sidebar-border: oklch(0.9271 0.0101 238.5177);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.15);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--tracking-normal: 0em;
--background: oklch(1 0 0);
--foreground: oklch(0.1884 0.0128 248.5103);
}
.dark {
--header-height: 64px;
--background: oklch(0 0 0);
--foreground: oklch(0.9328 0.0025 228.7857);
--card: oklch(0.2097 0.008 274.5332);
--card-foreground: oklch(0.8853 0 0);
--popover: oklch(0 0 0);
--popover-foreground: oklch(0.9328 0.0025 228.7857);
--primary: oklch(0.6692 0.1607 245.011);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.9622 0.0035 219.5331);
--secondary-foreground: oklch(0.1884 0.0128 248.5103);
--muted: oklch(0.209 0 0);
--muted-foreground: oklch(0.5637 0.0078 247.9662);
--accent: oklch(0.1928 0.0331 242.5459);
--accent-foreground: oklch(0.6692 0.1607 245.011);
--destructive: oklch(0.6188 0.2376 25.7658);
--border: oklch(0.2674 0.0047 248.0045);
--input: oklch(0.302 0.0288 244.8244);
--ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
--chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.2097 0.008 274.5332);
--sidebar-foreground: oklch(0.8853 0 0);
--sidebar-primary: oklch(0.6818 0.1584 243.354);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.1928 0.0331 242.5459);
--sidebar-accent-foreground: oklch(0.6692 0.1607 245.011);
--sidebar-border: oklch(0.3795 0.022 240.5943);
--sidebar-ring: oklch(0.6818 0.1584 243.354);
--destructive-foreground: oklch(1 0 0);
--radius: 1.3rem;
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--shadow-color: rgba(29, 161, 242, 0.25);
--shadow-opacity: 0;
--shadow-blur: 0px;
--shadow-spread: 0px;
--shadow-offset-x: 0px;
--shadow-offset-y: 2px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-sm:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-md:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-lg:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-xl:
0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0),
0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0);
--shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
}
}
.no-scrollbar {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.no-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.radial-gradient {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.radial-gradient:hover {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 0.5);
}
.radial-gradient-border-button {
background: radial-gradient(
circle at 50% 0%,
rgba(250, 250, 250, 0.05) 0%,
transparent 60%
)
rgba(15, 15, 15, 1);
transition: background 0.3s ease-in-out;
width: fit-content;
}
.linear-mask {
mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
-webkit-mask-image: linear-gradient(
-75deg,
white calc(var(--x) + 20%),
transparent calc(var(--x) + 30%),
white calc(var(--x) + 100%)
);
}
.linear-overlay {
background-image: linear-gradient(
-75deg,
rgba(255, 255, 255, 0.1) calc(var(--x) + 20%),
rgba(255, 255, 255, 0.5) calc(var(--x) + 25%),
rgba(255, 255, 255, 0.1) calc(var(--x) + 100%)
);
mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
-webkit-mask:
linear-gradient(black, black) content-box,
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: xor;
padding: 1px;
}
@keyframes move-x {
from {
transform: translateX(var(--move-x-from));
}
to {
transform: translateX(var(--move-x-to));
}
}
.cpu-architecture {
offset-anchor: 10px 0px;
animation: animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
}
.cpu-line-1 {
offset-path: path("M 10 20 h 79.5 q 5 0 5 5 v 30");
animation-duration: 5s;
animation-delay: 1s;
}
.cpu-line-2 {
offset-path: path("M 180 10 h -69.7 q -5 0 -5 5 v 40");
animation-delay: 6s;
animation-duration: 2s;
}
.cpu-line-3 {
offset-path: path("M 130 20 v 21.8 q 0 5 -5 5 h -25");
animation-delay: 4s;
animation-duration: 6s;
}
.cpu-line-4 {
offset-path: path("M 170 80 v -21.8 q 0 -5 -5 -5 h -65");
animation-delay: 3s;
animation-duration: 3s;
}
.cpu-line-5 {
offset-path: path(
"M 135 65 h 15 q 5 0 5 5 v 10 q 0 5 -5 5 h -39.8 q -5 0 -5 -5 v -35"
);
animation-delay: 9s;
animation-duration: 4s;
}
.cpu-line-6 {
offset-path: path("M 94.8 95 v -46");
animation-delay: 3s;
animation-duration: 7s;
}
.cpu-line-7 {
offset-path: path(
"M 88 88 v -15 q 0 -5 -5 -5 h -10 q -5 0 -5 -5 v -5 q 0 -5 5 -5 h 28"
);
animation-delay: 4s;
animation-duration: 4s;
}
.cpu-line-8 {
offset-path: path("M 30 30 h 25 q 5 0 5 5 v 6.5 q 0 5 5 5 h 35");
animation-delay: 3s;
animation-duration: 3s;
}
@keyframes animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
.shadcn {
offset-anchor: 10px 0px;
animation: shadcn-animation-path;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.75, -0.01, 0, 0.99);
animation-duration: 4s;
animation-delay: 1.5s;
}
.shadcn-line-1 {
offset-path: path("M 35 14 h 25 q 5 0 5 5 v 23 q 0 5 5 5 h 25");
}
.shadcn-line-2 {
offset-path: path("M 30 50 h 65");
}
.shadcn-line-3 {
offset-path: path("M 36.3 87 h 24 q 5 0 5 -5 v -24 q 0 -5 5 -5 h 25");
}
.compBox {
animation: shadncn-animation-path-backwards;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.57, 0.02, 0, 1);
offset-path: path("M 167 36 h -17 q -5 0 -5 5 v 4 q 0 5 -5 5 h -32");
animation-duration: 4s;
opacity: 0;
animation-delay: 5s;
}
@keyframes shadncn-animation-path-backwards {
0% {
offset-distance: 100%;
opacity: 0;
}
90% {
opacity: 1;
offset-distance: 0%;
}
100% {
opacity: 0;
}
}
@keyframes shadcn-animation-path {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
@keyframes landing-marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.animate-landing-marquee {
animation: landing-marquee var(--marquee-duration, 45s) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-landing-marquee {
animation: none;
}
}
THIRD-PARTY-LICENSES.txt실행 안내·자료
react 19.2.3 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
scheduler 0.27.0 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
react-dom 19.2.3 — LICENSE
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
tailwindcss 4.1.13 — LICENSE
MIT License
Copyright (c) Tailwind Labs, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
========================================================================
tw-animate-css 1.3.8 — LICENSE
MIT License
Copyright (c) 2025 Wombosvideo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
provenance.json실행 안내·자료
{
"id": "21st-f23acde75a07",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"demoIdentity": {
"file": "baseline-example.tsx",
"export": "default",
"source": "exact-public-demo"
},
"fidelity": {
"preserved": "21st 공개 discovery의 고유 데모 ID와 미리보기 이미지가 가리키는 원래 데모 바이트 전체",
"dependency_revision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"observed_differences": [
"원래 CDN demo의 레이아웃 및 인수를 유지하고, 별도 component는 조사 시점 고정 작성자 revision에서 연결한다."
],
"mapping": [
"21st 설치 별칭 @/components/ui/inverted-cursor → 작성자 registry/gammaui/inverted-cursor.tsx; 문서 ComponentSource의 이름과 export/props 대조"
]
},
"acquisitionLimitations": [
"CDN 데모는 SHA256으로, 작성자 원본과 의존 파일은 Git 커밋 및 blob SHA로 고정했다.",
"현재 작성자 revision의 의존 파일이 과거 21st 내부 구현과 바이트 단위로 같다는 주장은 하지 않는다.",
"다운로드한 코드를 실행하지 않았으며 브라우저·상호작용 검증은 별도 단계다."
],
"files": [
{
"file": "baseline-example.tsx",
"kind": "implementation",
"sha256": "6300047e5334f70681b8cc2371421731ed87602940aaf86c4c9ab2f9692e1ce8",
"sourceRevision": "sha256:6300047e5334f70681b8cc2371421731ed87602940aaf86c4c9ab2f9692e1ce8",
"license": "MIT"
},
{
"file": "LICENSE",
"kind": "license",
"sha256": "185f2cd440066381aa1fb70411e11e6c288a961a91bc5274e92a72ee245f83ed",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "upstream/apps/www/content/docs/components/inverted-cursor.mdx",
"kind": "upstream-implementation",
"sha256": "a919c766732e044e7f5adcb76ad609714aa21a7aa1422a1362e6d57df982f3d4",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "upstream/apps/www/registry/example/inverted-cursor-demo.tsx",
"kind": "upstream-implementation",
"sha256": "9bf77eece093552d191bca2b2761c10909356f2c7ef03904bcacf50042ae61b2",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/registry/gammaui/inverted-cursor.tsx",
"kind": "implementation-dependency",
"sha256": "f39fa44a2a3f19d399f535afa08328003939e07ba6f210829d62483eb012d072",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/app/globals.css",
"kind": "theme-source",
"sha256": "d48b78e343a4a866a9ccc1137ca3ac2418ebf221bf52ab79d70759426f091628",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/package.json",
"kind": "dependency-manifest",
"sha256": "f3307ab08f84659d3850ff0d9aa47e10999581e3536af88acef876b1cd1099fa",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
}
],
"importMap": {
"@/components/ui/inverted-cursor": "author/apps/www/registry/gammaui/inverted-cursor.tsx"
},
"declaredDependencies": {
"react": "^19.2.3"
},
"runtimeDependencies": {
"react": "19.2.3",
"scheduler": "0.27.0",
"react-dom": "19.2.3",
"tailwindcss": "4.1.13",
"tw-animate-css": "1.3.8"
},
"assets": [],
"assetAdaptations": [],
"adaptations": [
"The exact original cursor demonstration, 60px default diameter, and pointer tracking are unchanged."
],
"runtime_verified": false
}
README.md실행 안내·자료
# Inverted Cursor · Gamma UI
The acquired original files are unchanged. Source revision: c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd. Each exact file hash and any demo content revision is recorded in provenance.json. Keep all included license notices.
- The exact original cursor demonstration, 60px default diameter, and pointer tracking are unchanged.
- Host spacing and fallback tokens come from runtime/author-styles.css. runtime/applied-theme.css contains the selected original author theme and overrides fallback tokens.
Open the standalone HTML to run with all JavaScript and CSS bundled locally. ?theme=light and ?theme=dark select the host theme. The parent gallery must use an opaque allow-scripts sandbox. No external requests are required.
For source reuse, start with Usage.tsx and the unchanged baseline demonstration. Resolve @/ imports to their included matching author files and install the exact package versions in provenance.json. The host mount and styles are supplied for reference.
Build success and the ready message do not prove runtime or accessibility behavior. Browser validation is recorded separately.
BEHIND THE EXAMPLE
이 장면을 만드는 원리
컨테이너 좌표를 프레임마다 따라가는 원
눈에 보이는 지연을 코드의 갱신 방식과 연결하되 성능은 실제 추적으로 측정합니다.
- 이 예제에서는
- Cursor는 mousemove의 client 좌표에서 컨테이너 rect를 빼고 size/2로 원의 중심을 맞춥니다. 매 requestAnimationFrame에서 남은 거리의 0.2만큼 움직이며 경과 시간을 인자로 쓰지 않습니다. 이는 프레임별 보간 설정이고 특정 밀리초 지연이나 장치 공통 속도를 보장하지 않습니다.
코드와 함께 확인하기
코드에서 찾기
x: e.clientX - rect.left,inverted-cursor.tsx포인터 위치를 컨테이너 내부 좌표로 바꿉니다.
const deltaX = (targetX - currentX) * 0.2inverted-cursor.tsx프레임마다 남은 거리에 비례해 이동합니다.
직접 해보기
컨테이너를 페이지 중앙에 두고 네 모서리와 중앙으로 포인터를 이동합니다.
살펴볼 변화문서 전체 좌표가 아닌 컨테이너 좌표로 중심을 따라가는지 확인합니다.
같은 포인터 이동을 정상 상태와 프레임 지연이 있는 상태에서 기록합니다.
살펴볼 변화수렴 시간과 프레임별 이동을 각각 측정합니다. 0.2라는 계수를 고정 시간 지연이나 성능 수치로 바꾸어 설명하지 않습니다.
위치 갱신과 프레임 루프의 수명
입력이 반복되거나 표면이 제거될 때 리스너와 활성 효과의 수명을 확인합니다.
- 이 예제에서는
- effect의 의존성은 position과 size이므로 위치가 바뀌면 세 마우스 리스너와 프레임 예약을 정리하고 다시 설정합니다. cleanup은 cancelAnimationFrame을 호출합니다. 한편 animate는 원이 보이지 않거나 목표에 도착한 경우에도 다음 프레임을 예약하며 별도 정지 조건이 없습니다.
코드와 함께 확인하기
코드에서 찾기
}, [position, size])inverted-cursor.tsx위치 변경마다 effect가 다시 실행되는 구조입니다.
if (requestRef.current) cancelAnimationFrame(requestRef.current)inverted-cursor.tsx정리 경로에서 예약된 프레임을 취소합니다.
requestRef.current = requestAnimationFrame(animate)inverted-cursor.tsx애니메이션 루프가 다음 프레임을 계속 요청합니다.
직접 해보기
포인터를 빠르게 움직이다 컴포넌트를 제거하고 다시 마운트합니다.
살펴볼 변화제거된 인스턴스의 리스너·프레임이 남지 않고 새 인스턴스만 갱신하는지 확인합니다.
포인터를 멈춘 뒤 영역을 벗어나며 프레임 호출을 관찰합니다.
살펴볼 변화원본은 수렴·비가시 상태에서 루프를 멈추지 않습니다. 실제 호출과 비용을 기록하고 시각적으로 안 보인다는 이유로 정지했다고 보고하지 않습니다.
숨긴 기본 커서와 장식 레이어의 입력 범위
장식 포인터를 추가해도 콘텐츠의 의미와 실제 입력 경로를 보존해야 합니다.
- 이 예제에서는
- Cursor의 바깥 레이어는 absolute inset-0 cursor-none이고, 안쪽 원만 pointer-events-none과 aria-hidden=true입니다. 입력은 mousemove·mouseenter·mouseleave만 구독합니다. baseline은 h-96 영역 안에 Move your mouse 제목을 두며 키보드·터치·모션 감소에 따른 별도 커서 경로는 이 컴포넌트에 없습니다.
코드와 함께 확인하기
코드에서 찾기
className="absolute inset-0 cursor-none"inverted-cursor.tsx부모 영역을 덮는 레이어가 기본 커서를 숨깁니다.
aria-hidden="true"inverted-cursor.tsx장식 원은 접근성 정보에서 제외하도록 선언합니다.
<main className="flex h-full items-center justify-center">baseline-example.tsx제목의 읽기 구조는 장식 원과 별도입니다.
직접 해보기
원본과 분리된 소비 예제에 링크를 넣고 포인터·키보드로 작동시킵니다.
살펴볼 변화바깥 오버레이가 링크 입력을 가로막는지 확인합니다. 안쪽 원의 pointer-events-none만으로 전체 레이어가 입력을 통과한다고 보지 않습니다.
마우스가 없는 입력 경로와 움직임 감소 환경에서 콘텐츠와 포인터 피드백을 확인합니다.
살펴볼 변화콘텐츠를 읽고 조작하는 경로가 남아야 합니다. 원본에 별도 대체 커서 경로가 있다는 주장은 하지 않습니다.
