21st.dev 원본
Logo Timeline · Gamma UI
데이터 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
저장한 HTML에서는 갤러리의 재생 설정이 적용되지 않아요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다. 실행 HTML에는 미리보기를 위한 실행 환경이 들어 있습니다.
baseline-example.tsx
import {
IconBrandApple,
IconBrandDiscord,
IconBrandGithub,
IconBrandGoogleDrive,
IconBrandMessenger,
IconBrandNotion,
IconBrandOpenai,
IconBrandPaypal,
IconBrandReact,
IconBrandTailwind,
IconBrandTypescript,
IconBrandWhatsapp,
IconBrandX,
} from "@tabler/icons-react"
import { LogoTimeline } from "@/components/ui/logo-timeline"
import type { LogoItem } from "@/components/ui/logo-timeline"
const logos: LogoItem[] = [
// Row 1 - Communication & Social (2 logos, 50s duration, spaced 25s apart)
{
label: "Discord",
icon: <IconBrandDiscord />,
animationDelay: -50,
animationDuration: 50,
row: 1,
},
{
label: "X (Twitter)",
icon: <IconBrandX />,
animationDelay: -25,
animationDuration: 50,
row: 1,
},
// Row 2 - Development Tools (2 logos, 45s duration, spaced 22.5s apart)
{
label: "GitHub",
icon: <IconBrandGithub />,
animationDelay: -45,
animationDuration: 45,
row: 2,
},
{
label: "React",
icon: <IconBrandReact />,
animationDelay: -22.5,
animationDuration: 45,
row: 2,
},
// Row 3 - Development Tools Continued (3 logos, 60s duration, spaced 20s apart)
{
label: "TypeScript",
icon: <IconBrandTypescript />,
animationDelay: -60,
animationDuration: 60,
row: 3,
},
{
label: "Tailwind",
icon: <IconBrandTailwind />,
animationDelay: -40,
animationDuration: 60,
row: 3,
},
// Row 4 - Productivity & Cloud (2 logos, 55s duration, spaced 27.5s apart)
{
label: "Google Drive",
icon: <IconBrandGoogleDrive />,
animationDelay: -55,
animationDuration: 55,
row: 4,
},
{
label: "Notion",
icon: <IconBrandNotion />,
animationDelay: -27.5,
animationDuration: 55,
row: 4,
},
// Row 5 - Messaging (2 logos, 50s duration, spaced 25s apart)
{
label: "WhatsApp",
icon: <IconBrandWhatsapp />,
animationDelay: -50,
animationDuration: 50,
row: 5,
},
{
label: "Messenger",
icon: <IconBrandMessenger />, // Placeholder icon
animationDelay: -25,
animationDuration: 50,
row: 5,
},
// Row 6 - AI & Automation (3 logos, 65s duration, spaced ~21.5s apart)
{
label: "OpenAI",
icon: <IconBrandOpenai />, // Placeholder icon
animationDelay: -65,
animationDuration: 65,
row: 6,
},
// Row 7 - Payment & Services (2 logos, 50s duration, spaced 25s apart)
{
label: "PayPal",
icon: <IconBrandPaypal />, // Placeholder icon
animationDelay: -50,
animationDuration: 50,
row: 7,
},
{
label: "Apple",
icon: <IconBrandApple />, // Placeholder icon
animationDelay: -25,
animationDuration: 50,
row: 7,
},
]
export function LogoTimelineDemo() {
return (
<div className="w-full overflow-hidden">
<LogoTimeline
items={logos}
title="Built with the best tools"
height="h-[400px] sm:h-[400px]"
iconSize={18}
showRowSeparator={true}
/>
</div>
)
}
export default LogoTimelineDemo
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.
함께 쓰는 파일 14개 보기
upstream/apps/www/content/docs/components/logo-timeline.mdx
---
title: Logo Timeline
date: 2025-11-01
description: A visually stunning animated logo timeline component that displays logos in horizontal scrolling rows with smooth infinite animations, perfect for showcasing partners, technologies, or integrations.
author: mazyar
published: true
---
import { CSSCodeBlock } from "@/components/css-code-block"
<ComponentPreview name="logo-timeline-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 @gammaui/logo-timeline
```
<CSSCodeBlock
fileName="global.css"
code={`
@keyframes move-x {
from {
transform: translateX(var(--move-x-from));
}
to {
transform: translateX(var(--move-x-to));
}
}`}
copyCode={true}
/>
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource
name="logo-timeline"
title="components/gammaui/logo-timeline.tsx"
/>
<Step>Add the following CSS to your global CSS file.</Step>
<CSSCodeBlock
fileName="global.css"
code={`
@keyframes move-x {
from {
transform: translateX(var(--move-x-from));
}
to {
transform: translateX(var(--move-x-to));
}
}`}
copyCode={true}
/>
<Step>Update the import paths to match your project setup.</Step>
</Steps>
</TabsContent>
</Tabs>
## Examples
### With Hover Animation
<ComponentPreview name="logo-timeline-demo-2" />
## Usage
```tsx showLineNumbers
import { LogoTimeline, type LogoItem } from "@/components/gammaui/logo-timeline"
```
```tsx showLineNumbers
<LogoTimeline
items={logos}
height="h-[500px] sm:h-[600px]"
iconSize={18}
showRowSeparator={true}
/>
```
### Hover Animation Example
Animate logos only on hover for an interactive experience:
```tsx showLineNumbers {4}
<LogoTimeline
items={logos}
height="h-[400px]"
animateOnHover={true}
iconSize={20}
/>
```
### With Title
Add a centered title overlay for additional context:
```tsx showLineNumbers {3}
<LogoTimeline
items={logos}
title="Trusted by Industry Leaders"
height="h-[600px]"
iconSize={18}
showRowSeparator={true}
/>
```
## Props
| Prop | Type | Default | Description |
| ------------------ | ------------ | -------------------------- | ---------------------------------------------------------------- |
| `items` | `LogoItem[]` | `-` | Array of logo items to display on the timeline (required) |
| `title` | `string` | `-` | Optional title text displayed in the center of the timeline |
| `height` | `string` | `"h-[400px] sm:h-[800px]"` | Height of the timeline container (e.g., "h-[400px]", "h-screen") |
| `className` | `string` | `-` | Additional CSS classes for the container |
| `iconSize` | `number` | `16` | Size of icons in pixels |
| `showRowSeparator` | `boolean` | `true` | Whether to display separator lines between rows |
| `animateOnHover` | `boolean` | `false` | If true, animations only play when the component is hovered |
## LogoItem Interface
| Prop | Type | Default | Description |
| ------------------- | -------------------- | ------- | -------------------------------------------------------------------- |
| `label` | `string` | `-` | Label text displayed next to the icon |
| `icon` | `keyof typeof Icons` | `-` | Icon key from the Icons object (e.g., "discord", "gitHub", "google") |
| `animationDelay` | `number` | `-` | Animation delay in seconds (typically negative, e.g., -23) |
| `animationDuration` | `number` | `-` | Animation duration in seconds (e.g., 30, 40, 45, 60) |
| `row` | `number` | `-` | Row index (1-based) to group logos on the same timeline row |
## Notes
- **Animation Timing**: Use negative `animationDelay` values to stagger logo animations and create a natural flow. The delay determines when each logo starts its animation cycle.
- **Row Organization**: Logos are grouped by their `row` property. Each row can have multiple logos with different animation timings to create a seamless, infinite scrolling effect.
- **Icon Selection**: Icons must be valid keys from the `Icons` object exported from `@/components/icons`. Common icons include: `react`, `ts`, `tailwind`, `gitHub`, `discord`, `openai`, `v0`, `radix`, and more.
- **Animation Duration**: Longer durations create slower, more elegant animations. Typical values range from 30-65 seconds. Logos on the same row should use the same duration for synchronized movement.
- **Responsive Design**: The component is fully responsive and works well on mobile devices. Use Tailwind's responsive height utilities (e.g., `h-[400px] sm:h-[600px]`) for optimal viewing across screen sizes.
- **Performance**: The component uses CSS animations for smooth, performant scrolling without JavaScript overhead.
upstream/apps/www/registry/example/logo-timeline-demo.tsx
import {
IconBrandApple,
IconBrandDiscord,
IconBrandGithub,
IconBrandGoogleDrive,
IconBrandMessenger,
IconBrandNotion,
IconBrandOpenai,
IconBrandPaypal,
IconBrandReact,
IconBrandTailwind,
IconBrandTypescript,
IconBrandWhatsapp,
IconBrandX,
} from "@tabler/icons-react"
import { LogoTimeline } from "@/registry/gammaui/logo-timeline"
import type { LogoItem } from "@/registry/gammaui/logo-timeline"
const logos: LogoItem[] = [
// Row 1 - Communication & Social (2 logos, 50s duration, spaced 25s apart)
{
label: "Discord",
icon: <IconBrandDiscord />,
animationDelay: -50,
animationDuration: 50,
row: 1,
},
{
label: "X (Twitter)",
icon: <IconBrandX />,
animationDelay: -25,
animationDuration: 50,
row: 1,
},
// Row 2 - Development Tools (2 logos, 45s duration, spaced 22.5s apart)
{
label: "GitHub",
icon: <IconBrandGithub />,
animationDelay: -45,
animationDuration: 45,
row: 2,
},
{
label: "React",
icon: <IconBrandReact />,
animationDelay: -22.5,
animationDuration: 45,
row: 2,
},
// Row 3 - Development Tools Continued (3 logos, 60s duration, spaced 20s apart)
{
label: "TypeScript",
icon: <IconBrandTypescript />,
animationDelay: -60,
animationDuration: 60,
row: 3,
},
{
label: "Tailwind",
icon: <IconBrandTailwind />,
animationDelay: -40,
animationDuration: 60,
row: 3,
},
// Row 4 - Productivity & Cloud (2 logos, 55s duration, spaced 27.5s apart)
{
label: "Google Drive",
icon: <IconBrandGoogleDrive />,
animationDelay: -55,
animationDuration: 55,
row: 4,
},
{
label: "Notion",
icon: <IconBrandNotion />,
animationDelay: -27.5,
animationDuration: 55,
row: 4,
},
// Row 5 - Messaging (2 logos, 50s duration, spaced 25s apart)
{
label: "WhatsApp",
icon: <IconBrandWhatsapp />,
animationDelay: -50,
animationDuration: 50,
row: 5,
},
{
label: "Messenger",
icon: <IconBrandMessenger />, // Placeholder icon
animationDelay: -25,
animationDuration: 50,
row: 5,
},
// Row 6 - AI & Automation (3 logos, 65s duration, spaced ~21.5s apart)
{
label: "OpenAI",
icon: <IconBrandOpenai />, // Placeholder icon
animationDelay: -65,
animationDuration: 65,
row: 6,
},
// Row 7 - Payment & Services (2 logos, 50s duration, spaced 25s apart)
{
label: "PayPal",
icon: <IconBrandPaypal />, // Placeholder icon
animationDelay: -50,
animationDuration: 50,
row: 7,
},
{
label: "Apple",
icon: <IconBrandApple />, // Placeholder icon
animationDelay: -25,
animationDuration: 50,
row: 7,
},
]
export function LogoTimelineDemo() {
return (
<div className="w-full overflow-hidden">
<LogoTimeline
items={logos}
title="Built with the best tools"
height="h-[400px] sm:h-[400px]"
iconSize={18}
showRowSeparator={true}
/>
</div>
)
}
author/apps/www/registry/gammaui/logo-timeline.tsx
"use client"
import type React from "react"
import { useState } from "react"
import { motion } from "motion/react"
import { cn } from "@/lib/utils"
export interface LogoItem {
/** The label text displayed next to the icon */
label: string
/** The icon name from the Icons object (e.g., "gitHub", "react", "tailwind") */
icon: React.ReactNode
/** Animation delay in seconds (use negative values for staggered effect) */
animationDelay: number
/** Animation duration in seconds */
animationDuration: number
/** The row number where this logo should appear (1-based) */
row: number
}
export interface LogoTimelineProps {
/** Array of logo items to display */
items: LogoItem[]
/** Optional title text to display in the center */
title?: string
/** Height of the timeline container */
height?: string
/** Additional className for the container */
className?: string
/** Icon size in pixels (default: 16) */
iconSize?: number
/** Whether to show separator lines between rows (default: true) */
showRowSeparator?: boolean
/** Whether to animate logos only on hover (default: false) */
animateOnHover?: boolean
}
export function LogoTimeline({
items,
title,
height = "h-[400px] sm:h-[800px]",
className,
iconSize = 16,
showRowSeparator = true,
animateOnHover = false,
}: LogoTimelineProps) {
const [isHovered, setIsHovered] = useState(false)
// Group items by row
const rowsMap = new Map<number, LogoItem[]>()
items.forEach((item) => {
if (!rowsMap.has(item.row)) {
rowsMap.set(item.row, [])
}
rowsMap.get(item.row)?.push(item)
})
// Convert map to sorted array
const rows = Array.from(rowsMap.entries())
.sort(([a], [b]) => a - b)
.map(([, rowItems]) => rowItems)
// Determine animation play state
const animationPlayState = animateOnHover
? isHovered
? "running"
: "paused"
: "running"
return (
<section className={cn("w-full", height, className)}>
<motion.div
aria-hidden="true"
className="bg-background relative h-full w-full overflow-hidden py-24 ring-inset sm:py-32"
onMouseEnter={() => animateOnHover && setIsHovered(true)}
onMouseLeave={() => animateOnHover && setIsHovered(false)}
>
{title && (
<div className="absolute top-1/2 left-1/2 mx-auto w-full max-w-[90%] -translate-x-1/2 -translate-y-1/2 text-center">
<div className="relative z-10">
<p className="text-foreground/10 mx-auto mt-2 max-w-3xl text-4xl font-semibold tracking-tight text-pretty sm:text-5xl md:text-6xl">
{title}
</p>
</div>
</div>
)}
<div
className="@container absolute inset-0 grid"
style={{ gridTemplateRows: `repeat(${rows.length}, 1fr)` }}
>
{rows.map((rowItems, index) => (
<div className="group relative flex items-center" key={index}>
<div className="from-foreground/15 dark:from-foreground/15 absolute inset-x-0 top-1/2 h-0.5 bg-linear-to-r from-[2px] to-[2px] bg-size-[12px_100%]" />
{showRowSeparator && (
<div className="from-foreground/5 dark:from-foreground/5 absolute inset-x-0 bottom-0 h-0.5 bg-linear-to-r from-[2px] to-[2px] bg-size-[12px_100%] group-last:hidden" />
)}
{rowItems.map((logo) => {
return (
<div
key={`${logo.row}-${logo.label}`}
className={cn(
"absolute top-1/2 flex -translate-y-1/2 items-center gap-2 px-3 py-1.5 whitespace-nowrap",
"ring-background/10 dark:ring-foreground/10 rounded-full bg-linear-to-t from-white/50 from-50% to-white/50 ring-1 backdrop-blur-sm ring-inset dark:from-neutral-900 dark:to-gray-900",
"repeat-[infinite] [--move-x-from:-100%] [--move-x-to:calc(100%+100cqw)] [animation-name:move-x] [animation-timing-function:linear]",
"shadow-[0_0_15px_rgba(0,0,0,0.1)] dark:shadow-none"
)}
style={{
animationDelay: `${logo.animationDelay}s`,
animationDuration: `${logo.animationDuration}s`,
animationPlayState: animationPlayState,
}}
>
{logo.icon}
<span className="text-foreground text-sm/6 font-medium">
{logo.label}
</span>
</div>
)
})}
</div>
))}
</div>
</motion.div>
</section>
)
}
author/apps/www/lib/utils.ts
import process from "process"
import { Metadata } from "next"
import clsx, { ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export const pluralize = (count: number, word: string) =>
`${count} ${word}${count === 1 ? "" : "s"}`
export function humanize(name: string): string {
return name
.replace(/-/g, " ")
.replace(/([A-Z])/g, " $1")
.trim()
.split(/\s+/)
.map((word) => word[0].toUpperCase() + word.substring(1).toLowerCase())
.join(" ")
}
export const truncate = (str: string | null, length: number) => {
if (!str || str.length <= length) return str
return `${str.slice(0, length - 3)}...`
}
export const fetcher = (...args: Parameters<typeof fetch>) =>
fetch(...args).then((res) => res.json())
/**
* Capitalizes first letters of words in string.
* @param {string} str String to be modified
* @param {boolean=false} lower Whether all other letters should be lowercased
* @return {string}
* @see https://stackoverflow.com/questions/2332811/capitalize-words-in-string/7592235#7592235
* @usage
* capitalize('fix this string'); // -> 'Fix This String'
* capitalize('javaSCrIPT'); // -> 'JavaSCrIPT'
* capitalize('javaSCrIPT', true); // -> 'Javascript'
*/
export const capitalize = (str: string, lower = false) =>
(lower ? str.toLowerCase() : str).replace(/(?:^|\s|["'([{])+\S/g, (match) =>
match.toUpperCase()
)
export function formatDate(input: string | number): string {
const date = new Date(input)
return date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
export const calculateReadingTime = (content: string): number => {
const words = content?.trim().split(/\s+/).filter(Boolean).length
return Math.max(1, Math.ceil(words / 200))
}
export const normalizeTag = (tag: unknown): string[] => {
if (!tag) return []
return Array.isArray(tag)
? tag.filter((t): t is string => typeof t === "string")
: [String(tag)]
}
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
}
export function constructMetadata({
title = "Gamma UI - Modern React + Tailwind CSS components & Templates",
description = "Beautifully designed landing page components built with React & Tailwind CSS & Motion.",
image = absoluteUrl("/og"),
...props
}: {
title?: string
description?: string
image?: string
[key: string]: Metadata[keyof Metadata]
}): Metadata {
return {
title,
description,
keywords: [
"GammaUI",
"UI Library",
"Component Library",
"React Components",
"Next.js",
"Tailwind CSS",
"Open Source",
"Frontend",
"Design System",
"Mazyar Kawa",
"GammaUI by Aman",
"Copy and Paste Components",
"Developer Tools",
"Frontend Engineer",
"Beautiful UI",
"React UI Kit",
"Free React Components",
"Open Source UI Kit",
"Tailwind UI Components",
"Headless UI",
"Reusable React Components",
"UI Templates",
"Accessible React Components",
"Copy-Paste UI",
"Next.js Component Library",
"Open Source Developer Tools",
"Frontend Design System",
"Minimal UI Kit",
"Clean React UI",
],
openGraph: {
title,
description,
type: "website",
images: [
{
url: image,
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [image],
creator: "@mazyar_kawa",
},
icons: "/favicon.ico",
metadataBase: new URL("https://gammaui.com"),
authors: [
{
name: "mazyar_kawa",
url: "https://twitter.com/mazyar_kawa",
},
],
creator: "mazyar_kawa",
...props,
}
}
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 {LogoTimelineDemo as 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;
}
}
runtime/process.ts실행 안내·자료
// Browser host for a Node-only import in unused upstream documentation helpers.
// Actual components consume the unchanged cn export; no provider environment is exposed.
export default { env: {} };
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.
========================================================================
@tabler/icons-react 3.35.0 — LICENSE
MIT License
Copyright (c) 2020-2025 Paweł Kuna
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.
========================================================================
framer-motion 12.43.0 — LICENSE.md
The MIT License (MIT)
Copyright (c) 2018 Framer B.V.
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.
========================================================================
motion-utils 12.39.0 — LICENSE.md
The MIT License (MIT)
Copyright (c) 2024 [Motion](https://motion.dev) B.V.
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.
========================================================================
motion-dom 12.43.0 — LICENSE.md
The MIT License (MIT)
Copyright (c) 2024 [Motion](https://motion.dev) B.V.
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.
========================================================================
motion 12.23.25 — LICENSE.md
The MIT License (MIT)
Copyright (c) 2024 [Motion](https://motion.dev) B.V.
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.
========================================================================
clsx 2.1.1 — license
MIT License
Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)
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.
========================================================================
tailwind-merge 3.3.1 — LICENSE.md
MIT License
Copyright (c) 2021 Dany Castillo
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-0bc8f3db6cb0",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"demoIdentity": {
"file": "baseline-example.tsx",
"export": "LogoTimelineDemo",
"source": "exact-public-demo"
},
"fidelity": {
"preserved": "21st 공개 discovery의 고유 데모 ID와 미리보기 이미지가 가리키는 원래 데모 바이트 전체",
"dependency_revision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"observed_differences": [
"원래 CDN demo의 레이아웃 및 인수를 유지하고, 별도 component는 조사 시점 고정 작성자 revision에서 연결한다."
],
"mapping": [
"21st 설치 별칭 @/components/ui/logo-timeline → 작성자 registry/gammaui/logo-timeline.tsx; 문서 ComponentSource의 이름과 export/props 대조"
]
},
"acquisitionLimitations": [
"CDN 데모는 SHA256으로, 작성자 원본과 의존 파일은 Git 커밋 및 blob SHA로 고정했다.",
"현재 작성자 revision의 의존 파일이 과거 21st 내부 구현과 바이트 단위로 같다는 주장은 하지 않는다.",
"다운로드한 코드를 실행하지 않았으며 브라우저·상호작용 검증은 별도 단계다."
],
"files": [
{
"file": "baseline-example.tsx",
"kind": "implementation",
"sha256": "57287e0cee28a4439f2d482019fb7a5aa9ca9ad5767dc2188b86df3b0b57ba0a",
"sourceRevision": "sha256:57287e0cee28a4439f2d482019fb7a5aa9ca9ad5767dc2188b86df3b0b57ba0a",
"license": "MIT"
},
{
"file": "LICENSE",
"kind": "license",
"sha256": "185f2cd440066381aa1fb70411e11e6c288a961a91bc5274e92a72ee245f83ed",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "upstream/apps/www/content/docs/components/logo-timeline.mdx",
"kind": "upstream-implementation",
"sha256": "061a8cfbd65e80395e292e1d795079187b5a21c6ac5e351074a161b90b0e71b5",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "upstream/apps/www/registry/example/logo-timeline-demo.tsx",
"kind": "upstream-implementation",
"sha256": "f94184a8373e9826bf701686d82c79060b6077ad7275a767c5ca6949951e0163",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/registry/gammaui/logo-timeline.tsx",
"kind": "implementation-dependency",
"sha256": "b3a2672638b6950e5fec804709c4b16569176c4a72c1d17ca37d2482e6040d1d",
"sourceRevision": "c79ba85279e4ff05a85b5ea4c2a8282c3467b6bd",
"license": "MIT"
},
{
"file": "author/apps/www/lib/utils.ts",
"kind": "implementation-dependency",
"sha256": "498451813b574b01a0eac9a1d5a96dfa261218ad55fff92ffc5afbf9fe785e22",
"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/logo-timeline": "author/apps/www/registry/gammaui/logo-timeline.tsx",
"@/lib/utils": "author/apps/www/lib/utils.ts"
},
"declaredDependencies": {
"@tabler/icons-react": "^3.35.0",
"clsx": "^2.1.1",
"motion": "^12.23.24",
"next": "15.2.6",
"process": null,
"react": "^19.2.3",
"tailwind-merge": "^3.3.1"
},
"runtimeDependencies": {
"react": "19.2.3",
"@tabler/icons-react": "3.35.0",
"framer-motion": "12.43.0",
"motion-utils": "12.39.0",
"motion-dom": "12.43.0",
"motion": "12.23.25",
"clsx": "2.1.1",
"tailwind-merge": "3.3.1",
"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 13-logo array, negative delays, seven rows, and 400px demonstration height are retained.",
"The original author CSS move-x keyframe is included; an empty browser process shim is provided for unused documentation helpers."
],
"runtime_verified": false
}
README.md실행 안내·자료
# Logo Timeline · 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 13-logo array, negative delays, seven rows, and 400px demonstration height are retained.
- The original author CSS move-x keyframe is included; an empty browser process shim is provided for unused documentation helpers.
- 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
이 장면을 만드는 원리
행 번호와 실제 지원하는 입력
공개 prop의 이름과 실제 소비 위치를 대조하고 데이터에서 계산하는 구조를 명시합니다.
- 이 예제에서는
- 원본 데모는 13개 로고를 일곱 행에 전달합니다. LogoTimeline은 row를 Map으로 묶고 숫자로 정렬한 뒤 존재하는 행 수만큼 균등 행을 만듭니다. 번호가 빠져도 빈 행을 예약하지 않습니다. 데모의 iconSize=18은 prop으로 전달되지만 컴포넌트 본문에서 아이콘에 적용하지 않고 logo.icon을 그대로 렌더링합니다.
코드와 함께 확인하기
코드에서 찾기
.sort(([a], [b]) => a - b)logo-timeline.tsx실제로 존재하는 행들을 번호순으로 정렬합니다.
style={{ gridTemplateRows: `repeat(${rows.length}, 1fr)` }}logo-timeline.tsx누락된 번호가 아닌 그룹 개수로 행을 만듭니다.
{logo.icon}logo-timeline.tsx전달된 React 노드를 그대로 넣으며 iconSize는 사용하지 않습니다.
직접 해보기
별도 소비 코드에서 row가 1과 3인 항목만 전달합니다.
살펴볼 변화빈 두 번째 번호 자리를 예약하지 않고 두 그룹을 배치하는지 확인합니다.
같은 icon 노드에서 iconSize만 16과 32로 바꿉니다.
살펴볼 변화해당 prop이 원본에서 아이콘 크기에 적용되지 않는지 확인합니다. prop 설명만으로 크기 조절을 지원한다고 약속하지 않습니다.
음수 지연과 hover 전용 재생 정책
자동 재생과 사용자가 제어하는 재생을 나누고 정지 뒤의 정보 접근을 확인합니다.
- 이 예제에서는
- 각 로고는 move-x CSS keyframes와 개별 duration·음수 delay를 받습니다. 이동 범위는 자체 폭 -100%에서 자체 폭과 컨테이너 폭 100cqw의 합까지입니다. animateOnHover 기본값은 false여서 실행 상태이며, true일 때는 마우스 진입·이탈만으로 running과 paused를 바꿉니다. 키보드 초점이나 시스템 선호를 처리하는 분기는 이 컴포넌트에 없습니다.
코드와 함께 확인하기
코드에서 찾기
[--move-x-to:calc(100%+100cqw)]logo-timeline.tsx이동 끝값은 로고 자체와 컨테이너의 폭을 함께 사용합니다.
const animationPlayState = animateOnHoverlogo-timeline.tsx재생 정책은 hover 선택과 현재 마우스 상태에서 계산합니다.
@keyframes move-x {globals.css시간에 따른 위치 변화는 작성자 CSS가 소유합니다.
직접 해보기
별도 소비 코드에서 animateOnHover를 켜고 마우스를 넣었다 뺀 뒤 키보드 초점으로만 접근합니다.
살펴볼 변화마우스 정지·재개와 키보드 경로의 차이를 기록합니다. hover 옵션을 모든 입력 방식의 재생 컨트롤로 소개하지 않습니다.
움직임 감소를 처음부터 켠 경우와 재생 중 켠 경우에 남은 로고를 확인합니다.
살펴볼 변화정지 시점의 잘린 화면만 남아 필수 정보가 사라지는지 확인해야 합니다. 멈췄다는 사실만으로 전체 로고의 정적 읽기 경로가 생긴다고 보지 않습니다.
