feat: initial commit

This commit is contained in:
Fortura Developer 2025-08-20 04:12:49 -06:00
commit fc8fb230e0
1263 changed files with 101379 additions and 0 deletions

11
.cursor/mcp.json Normal file
View File

@ -0,0 +1,11 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["-y", "shadcn@canary", "registry:mcp"],
"env": {
"REGISTRY_URL": "https://animate-ui.com/r/registry.json"
}
}
}
}

10
.eslintrc.js Normal file
View File

@ -0,0 +1,10 @@
// This configuration only applies to the package manager root.
/** @type {import("eslint").Linter.Config} */
module.exports = {
ignorePatterns: ['apps/**', 'packages/**'],
extends: ['@workspace/eslint-config/library.js'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
},
};

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build
dist
# Debug
npm-debug.log*
# Misc
.DS_Store
*.pem

1
.husky/commit-msg Executable file
View File

@ -0,0 +1 @@
pnpm exec commitlint --edit $1

1
.husky/pre-push Normal file
View File

@ -0,0 +1 @@
pnpm lint && pnpm build

0
.npmrc Normal file
View File

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

348
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,348 @@
# Contributing to Animate UI
Thank you for your interest in **contributing to Animate UI**! Your support is highly appreciated, and we look forward to your contributions. This guide will help you understand the project structure and provide detailed instructions for adding a new component or effect to Animate UI.
**Note:** You only need to modify a few files to add a new component, and it should take you around 10 minutes to complete.
## Getting Started
### Fork and Clone the Repository
#### 1. Fork the Repository
Click [here](https://github.com/animate-ui/animate-ui/fork) to fork the repository.
#### 2. Clone your Fork to Your Local Machine
```bash
git clone https://github.com/<YOUR_USERNAME>/animate-ui.git
```
#### 3. Navigate to the Project Directory
```bash
cd animate-ui
```
#### 4. Create a New Branch for Your Changes
```bash
git checkout -b my-branch
```
#### 5. Install Dependencies
```bash
pnpm i
```
#### 6. Run the Project
```bash
pnpm dev
```
## Edit a Component
If you need to modify a component to correct or improve it, you must :
- add a screenshot (photo or video as appropriate) of before and after the modification
- clearly explain why you made the modification
### Edit the code
Edit the component in the `registry` folder. Don't forget to adapt the demo and documentation if necessary.
You shouldn't change your behavior completely unless there's a good reason.
### Build the Registry
To update the registry, run the following command:
```bash
pnpm registry:build
```
## Adding a New Component
The addition of a new component must comply with certain rules:
- The component must be animated in some way (css, motion, ...).
- You can't just copy/paste component code from other libraries. You can be inspired by a component, but it must have added value. For example, I took Shadcn's components and animated them. So I didn't copy and paste the component, I added something to it.
- If you take inspiration from a component (CodePen, another library, etc.), remember to add the “Credits” section to your documentation. It's important to respect the work of other developers.
To submit your component, please include a demo video in the MR. Once the component has been submitted, it must be validated by @imskyleen.
To **add a new component to Animate UI**, you will need to update several files. Follow these steps:
### Create the Component
#### Basics
Create your main component in `apps/www/registry/[category]/my-component/index.tsx`.
```tsx title="my-component/index.tsx"
'use client';
import * as React from 'react';
type MyComponentProps = {
myProps: string;
} & React.ComponentProps<'div'>;
function MyComponent({ myProps, ...props }) {
return <div {...props}>{/* Your component */}</div>;
}
export { MyComponent, type MyComponentProps };
```
#### Registry item
Create a `apps/www/registry/[category]/my-component/registry-item.json` file to export your component :
```json title="my-component/registry-item.json"
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "my-component",
"type": "registry:ui",
"title": "My Component",
"description": "My Component Description",
"dependencies": [...],
"devDependencies": [...],
"files": [
{
"path": "registry/[category]/my-component/index.tsx",
"type": "registry:ui",
"target": "components/animate-ui/demo/[category]/my-component.tsx"
}
]
}
```
### Create a demo
#### Basics
Create your demo in `apps/www/registry/demo/[category]/my-component/index.tsx`.
```tsx title="my-component/index.tsx"
'use client';
import {
MyComponent,
type MyComponentProps,
} from '@/registry/[category]/my-component';
type MyComponentDemoProps = {
myProps: string;
} & MyComponentProps;
export const MyComponentDemo = ({ myProps }) => {
return <MyComponent myProps={myProps} />;
};
```
#### Registry item
```json title="my-component/registry-item.json"
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "my-component-demo",
"type": "registry:ui",
"title": "My Component Deo",
"description": "Demo showing my component",
"registryDependencies": ["https://animate-ui.com/r/my-component"],
"files": [
{
"path": "registry/demo/[category]/my-component/index.tsx",
"type": "registry:ui",
"target": "components/[category]/demo/my-component.tsx"
}
]
}
```
#### Add a Tweakpane
You can add a Tweakpane allowing users to play with your demo props.
Your demo must accept the props you want in your tweakpane :
```tsx title="my-component-demo/index.tsx"
import { MyComponent } from '@/registry/[category]/my-component';
type MyComponentDemoProps = {
props1: number;
props2: number;
props3: string;
props4: string;
props5: boolean;
};
export function MyComponentDemo({
props1,
props2,
props3,
props4,
props5,
}: MyComponentDemoProps) {
return <MyComponent />;
}
```
You must then specify the demo props information in your demo's `registry-item.json` file:
```json title="my-component-demo/registry-item.json"
{
...
"meta": {
"demoProps": {
"MyComponent": {
"props1": { "value": 700, "min": 0, "max": 2000, "step": 100 },
"props2": { "value": 0 },
"props3": { "value": "foo" },
"props4": {
"value": "center",
"options": {
"start": "start",
"center": "center",
"end": "end"
}
},
"props5": { "value": true }
}
}
},
...
}
```
**You need to run `pnpm registry:build` to see the updated tweakpane in the demo.**
#### How to use `demoProps`
##### Number
Simple number input:
```json
"myNumber": { "value": 10 }
```
Slider:
```json
"myNumber": { "value": 10, "min": 0, "max": 100, "step": 1 }
```
Select:
```json
"myNumber": {
"value": 10,
"options": {
"Big": 30,
"Medium": 20,
"Small": 10
}
}
```
##### String
Simple text input:
```json
"myString": { "value": "Hello World" }
```
Select:
```json
"myNumber": {
"value": "small",
"options": {
"Big": "big",
"Medium": "medium",
"Small": "small"
}
}
```
##### Boolean
```json
"myBoolean": { "value": true }
```
### Update the Documentation Sidebar
Add your component to the documentation sidebar by updating the file `content/docs/meta.json`.
```json title="meta.json"
{
"title": "Animate UI",
"root": true,
"pages": [
...,
"[category]/my-component"
...
]
}
```
### Create the Component Documentation
Create an MDX file to document your component in `content/docs/[category]/my-component.mdx`.
```mdx
---
title: My Component
description: Description for the new component
author:
name: your name
url: https://link-to-your-profile.com
new: true
---
<ComponentPreview name="my-component-demo" />
## Installation
<ComponentInstallation name="my-component" />
## Usage
[Basic usage of the component]
## Props
<TypeTable
type={{
myProps: {
description: 'Description for my props',
type: 'string',
required: true,
},
}}
/>
## Credits
- Credits to [you](https://link-to-your-profile.com) for creating the component
```
### Build the Registry
To update the registry, run the following command:
```bash
pnpm registry:build
```
## Ask for Help
If you need any assistance or have questions, please feel free to open a [GitHub issue](https://github.com/animate-ui/animate-ui/issues/new). We are here to help!
Thank you again for your contribution to Animate UI! We look forward to seeing your improvements and new components.

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Elliot Sutton
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.

45
README.md Normal file
View File

@ -0,0 +1,45 @@
<div align="center">
<h1 style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0">Fortura Data Solutions</h1>
<p align="center">
A high-performance, cost-effective, and privacy-first alternative to cloud hosting for enterprises.
</p>
<a href="https://github.com/fortura-data/landing-page/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/fortura-data/landing-page?style=for-the-badge"></a>
<a href="https://twitter.com/forturadatasol"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/forturadatasol?style=for-the-badge&logo=x"></a>
<a href="https://github.com/fortura-data/landing-page/blob/main/LICENSE.md"><img alt="License" src="https://img.shields.io/badge/License-Proprietary-blue.svg?style=for-the-badge"></a>
</div>
![hero](https://fortura-data.com/og-image.png)
## About Fortura Data Solutions
Fortura Data Solutions provides a modern, enterprise-grade infrastructure stack that allows organizations to **own their hardware, slash their cloud bills, and maintain full control over their data**.
We built our platform for high-demand environments like VFX studios, where petabytes of data move in real-time on tight budgets. Now, we're bringing that same performance and cost-efficiency to every business serious about their bottom line and data sovereignty.
### Why Fortura?
* **Cost Reduction:** Stop paying rent. Own the hardware and see your operational costs drop significantly, often achieving break-even within 12 months.
* **Performance:** Get cloud-level performance with sub-10ms latencies on colocated hardware, tailored to your workloads.
* **Data Ownership & Privacy:** Your data stays on your hardware, governed by your rules. AI-native workflows keep your data private, with zero leakage to third parties.
* **Integrated Stack:** We help you build on the tools you already use, wiring them together and automating the boring parts.
* **Minimal Retraining:** Our "desire paths" approach means we embed with your team, shape the system to how you *actually* work, then automate.
## Getting Started
This repository contains the code for the Fortura Data Solutions marketing website. It's built with Next.js, TypeScript, and Tailwind CSS.
For information on how to set up the development environment, run the site locally, or contribute, please see our [contributing guide](CONTRIBUTING.md).
## Documentation
Detailed documentation for Fortura's services, migration guides, and operational procedures is available to clients on our internal portal.
## Contributing
Visit our [contributing guide](CONTRIBUTING.md) to learn how to contribute to the website's codebase.
## License
This website's code is licensed under a proprietary license. See [LICENSE.md](LICENSE.md) for details.

28
apps/www/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# deps
/node_modules
# generated content
.contentlayer
.content-collections
.source
# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# others
.env*.local
.vercel
next-env.d.ts

29
apps/www/README.md Normal file
View File

@ -0,0 +1,29 @@
<div align="center">
<h1 style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0">Animate UI</h1>
<p align="center">
A fully animated, open-source component distribution built with React, TypeScript, Tailwind CSS, and Motion.
</p>
<a href="https://github.com/animate-ui/animate-ui/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/animate-ui/animate-ui?style=for-the-badge"></a>
<a href="https://twitter.com/animate_ui"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/animate_ui?style=for-the-badge&logo=x"></a>
<a href="https://github.com/animate-ui/animate-ui/blob/main/LICENSE.md"><img alt="License" src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge"></a>
</div>
![hero](https://animate-ui.com/og-image.png)
## Beta
Animate UI is currently in **Beta phase**, so it's possible that the components contain **bugs** and that these will be **modified regularly**.
## Documentation
Visit [animate-ui.com](https://animate-ui.com/docs) to view the documentation.
## Contributing
Visit our [contributing guide](https://github.com/animate-ui/animate-ui/blob/main/CONTRIBUTING.md) to learn how to contribute.
## License
Licensed under the [MIT license](https://github.com/animate-ui/animate-ui/blob/main/LICENSE.md).

View File

@ -0,0 +1 @@
// The content of this directory is autogenerated by the registry server.

View File

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,365 @@
// app/(home)/page.tsx
'use client';
import * as React from 'react';
// Import layout components from our new shared `ui` library
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
// Import the useTone hook to access the global tone setting
import { useTone } from '@workspace/ui/hooks/use-tone';
// Import Lucide icons for the value proposition section
import { DollarSignIcon, WrenchIcon, MapIcon, LockIcon, CloudOffIcon, ZapIcon } from 'lucide-react';
import { PlaceholderIcon } from '@workspace/ui/components/icons';
// Import UI components from our shared library
import { Button } from '@workspace/ui/components/ui';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
// Import new content components
import { MetricsKPI } from '@workspace/ui/components/content';
import { LogosMarquee } from '@workspace/ui/components/content';
import { Steps } from '@workspace/ui/components/content';
import { FeatureCard, CaseStudyCard } from '@workspace/ui/components/content/cards';
import { TrustSection } from '@workspace/ui/components/content';
import { FAQAccordion } from '@workspace/ui/components/interactive';
import { FinalCTASection } from '@workspace/ui/components/content';
export default function HomePage() {
const { tone } = useTone(); // Access the current tone
// Content definitions based on tone
const heroHeadline = tone === 'direct'
? "Own your infrastructure. Slash your bill."
: "Cut waste, keep performance.";
const heroSubcopyLine1 = tone === 'direct'
? "Your hosting provider is fucking you. Cloud rent bleeds $25k$500k/mo."
: "Cloud rent is bleeding your budget dry.";
const heroSubcopyLine2 = tone === 'direct'
? "Own the hardware. Keep the performance. Kill the waste."
: "Own your stack. Colocate it. Keep the performance, ditch the waste.";
const valuePropositions = tone === 'direct' ? [
{
icon: <DollarSignIcon className="size-6" />,
title: "Stop getting scammed.",
description: "Cloud rent bleeds $25k$500k/mo. Own the hardware. Keep the performance. Kill the waste."
},
{
icon: <WrenchIcon className="size-6" />,
title: "Build on what works.",
description: "Keep your stack. We wire it together and automate the boring."
},
{
icon: <MapIcon className="size-6" />,
title: "Pave desire paths.",
description: "We embed with your team, shape the system to how they actually work."
}
] : [
{
icon: <DollarSignIcon className="size-6" />,
title: "Cut waste, keep performance.",
description: "Move from renting to owning. Dramatically reduce monthly costs without sacrificing speed or reliability."
},
{
icon: <WrenchIcon className="size-6" />,
title: "Integrate, don't replace.",
description: "We work with your existing tools and workflows. No disruptive rip-and-replace projects."
},
{
icon: <MapIcon className="size-6" />,
title: "Build around real workflows.",
description: "We map your actual processes, then build automation that fits like a glove."
}
];
// Mock data for LogosMarquee
const logos = Array(6).fill(null).map((_, i) => ({
name: `Client ${i + 1}`,
icon: <PlaceholderIcon className="size-8 md:size-10" />
}));
// Mock data for Process Steps
const processSteps = [
{
title: "Discovery",
description: "We deep-dive into your current infrastructure, workloads, and pain points.",
youDo: "Share access to cloud billing, architecture docs, and team interviews.",
weDo: "Analyze costs, identify waste, and map technical dependencies."
},
{
title: "Shadow & Map",
description: "We observe how your teams actually work and map desire paths.",
youDo: "Participate in workflow observations and workshops.",
weDo: "Document real workflows, identify automation opportunities, and draft the future state."
},
{
title: "Architect",
description: "We design a tailored, owned infrastructure solution.",
youDo: "Review and approve the proposed architecture and migration plan.",
weDo: "Specify hardware, select software stack, and design for performance and cost."
},
{
title: "Pilot",
description: "We build and test a critical component in the new environment.",
youDo: "Provide feedback on performance, usability, and integration.",
weDo: "Deploy, test, and iterate on the pilot component with your team."
},
{
title: "Migrate",
description: "We execute the planned migration with zero downtime.",
youDo: "Validate functionality and performance post-migration.",
weDo: "Cutover workloads, decommission old systems, and optimize the new stack."
},
{
title: "Operate",
description: "We manage the infrastructure, you focus on your core business.",
youDo: "Use the system and provide ongoing feedback for improvements.",
weDo: "Monitor, maintain, upgrade, and support the infrastructure 24/7."
}
];
// Mock data for Case Studies
const caseStudies = [
{
title: "VFX Studio Migration",
description: "Migrated a major VFX studio from cloud rendering to a colocated HPC cluster, reducing costs by 65%.",
kpi1: { value: "65%", label: "Cost ↓" },
kpi2: { value: "2x", label: "Performance ↑" },
ctaText: "See build",
ctaHref: "/case-studies/vfx-studio"
},
{
title: "Legal Document Management",
description: "Consolidated scattered legal tech stack into a secure, private Nextcloud instance with AI search.",
kpi1: { value: "$120k/mo", label: "Saved" },
kpi2: { value: "0", label: "Egress Fees" },
ctaText: "See build",
ctaHref: "/case-studies/legal-firm"
},
{
title: "Research Data Pipeline",
description: "Built a local ML training environment for a research institute, eliminating data transfer bottlenecks.",
kpi1: { value: "90%", label: "Time-to-Train ↓" },
kpi2: { value: "Break-even: 8mo", label: "" },
ctaText: "See build",
ctaHref: "/case-studies/research-institute"
}
];
// Mock data for Trust Section
const trustItems = [
{
icon: <LockIcon className="size-5" />,
text: "Your data. Your hardware. Your rules."
},
{
icon: <CloudOffIcon className="size-5" />,
text: "Colocated. Monitored. Upgradable without downtime."
},
{
icon: <ZapIcon className="size-5" />,
text: "AI-native, privacy-first. No third-party model training."
}
];
// Mock data for FAQ
// (This is just for homepage preview; full FAQ is on /faq page)
const faqItems = [
{
question: "What uptime can I expect?",
answer: "We design for 99.9%+ uptime through redundant hardware, proactive monitoring, and colocation in Tier III+ facilities."
},
{
question: "How is support handled?",
answer: "You get direct access to our engineering team via Slack, email, or phone. We provide 24/7 monitoring and alerting."
}
];
return (
<div className="flex flex-col min-h-screen">
<Navbar />
{/* Hero Section */}
<Section className="flex flex-col items-center text-center py-20 md:py-32">
<Container maxWidth="3xl">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight">
{heroHeadline}
</h1>
<p className="mt-6 text-lg md:text-xl text-muted-foreground max-w-2xl">
{heroSubcopyLine1}
</p>
<p className="mt-2 text-lg md:text-xl text-muted-foreground max-w-2xl">
{heroSubcopyLine2}
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
<Button size="lg" variant="default" asChild>
<a href="/contact">Book Architecture Call</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/pricing">Run My Numbers</a>
</Button>
</div>
{/* KPI Metrics */}
<div className="mt-16 grid grid-cols-1 sm:grid-cols-3 gap-8 max-w-2xl mx-auto">
<MetricsKPI value="$350/mo" label="after year 1" />
<MetricsKPI value="65%" label="avg. cost ↓" />
<MetricsKPI value="12mo" label="typical break-even" />
</div>
</Container>
</Section>
{/* Value Proposition Section */}
<Section background="muted">
<Container>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{valuePropositions.map((item, index) => (
<Card key={index} className="flex flex-col">
<CardHeader>
<div className="mb-2 text-primary">
{item.icon}
</div>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent className="flex-grow">
<CardDescription>{item.description}</CardDescription>
</CardContent>
</Card>
))}
</div>
</Container>
</Section>
{/* Proof Strip / Logos Marquee */}
<Section>
<Container>
<h2 className="text-2xl font-bold text-center mb-12">Trusted by forward-thinking teams</h2>
<LogosMarquee logos={logos} />
</Container>
</Section>
{/* Process Snapshot */}
<Section background="muted">
<Container>
<Steps
steps={processSteps.slice(0, 3)} // Show only first 3 steps on homepage
title="Our 6-Step Process"
ctaText="See Full Process"
ctaHref="/process"
/>
</Container>
</Section>
{/* Case Study Rail */}
<Section>
<Container>
<div className="text-center mb-12">
<h2 className="text-2xl font-bold">Proven Results</h2>
<p className="mt-2 text-muted-foreground">
Real outcomes from real migrations.
</p>
</div>
<Grid columns={{ initial: 1, md: 3 }} gap="6">
{caseStudies.map((study, index) => (
<CaseStudyCard
key={index}
title={study.title}
description={study.description}
kpi1={study.kpi1}
kpi2={study.kpi2}
ctaText={study.ctaText}
ctaHref={study.ctaHref}
/>
))}
</Grid>
</Container>
</Section>
{/* Pricing Preview / Own vs Rent */}
<Section background="muted">
<Container>
<div className="text-center mb-12">
<h2 className="text-2xl font-bold">Own vs Rent</h2>
<p className="mt-2 text-muted-foreground max-w-2xl mx-auto">
Cloud is a rental agreement with infinite term. Owned hardware + colocation breaks even in under 12 months.
</p>
</div>
<div className="max-w-4xl mx-auto bg-card border rounded-xl p-6 md:p-8">
<h3 className="text-xl font-semibold mb-4">Why Owned Infrastructure Wins</h3>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">Capex:</span> You buy the hardware once. Depreciate over 3-5 years.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">Low Opex:</span> Colocation is ~$200/server/mo. Managed ops is ~$500/server/mo.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">No Egress Fees:</span> Move data freely within your network.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">Performance:</span> Direct-attached storage and local networks are faster than VPCs.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">No Vendor Lock-in:</span> Standard hardware and open-source software.</span>
</li>
</ul>
<div className="mt-6 p-4 bg-muted rounded-lg">
<p className="text-center font-medium">
Break-even is typically &lt; 12 months. After that, it's pure savings.
</p>
</div>
</div>
</Container>
</Section>
{/* Trust Section */}
<Section>
<Container>
<TrustSection items={trustItems} title="Your Data, Your Rules" />
</Container>
</Section>
{/* FAQ Accordion Preview */}
<Section background="muted">
<Container>
<div className="text-center mb-12">
<h2 className="text-2xl font-bold">Common Questions</h2>
<p className="mt-2 text-muted-foreground">
Answers to key concerns about migrating and self-hosting.
</p>
</div>
<FAQAccordion />
<div className="mt-8 text-center">
<Button variant="link" asChild>
<a href="/faq">See All FAQs</a>
</Button>
</div>
</Container>
</Section>
{/* Final CTA */}
<Section>
<Container>
<FinalCTASection
title="Ready to cut your cloud bill?"
description="Book a free architecture call to see how much you can save."
primaryCtaText="Book Architecture Call"
primaryCtaHref="/contact"
secondaryCtaText="Download Blueprint"
secondaryCtaHref="/download-blueprint" // Placeholder
/>
</Container>
</Section>
<Footer />
</div>
);
}

129
apps/www/app/about/page.tsx Normal file
View File

@ -0,0 +1,129 @@
// app/about/page.tsx
'use client';
import * as React from 'react';
import { ExternalLinkIcon, AwardIcon, UsersIcon, ZapIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { Callout } from '@workspace/ui/components/content';
import { Badge } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
export default function AboutPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">About Fortura</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
We built it for VFX first. That's why it works for everyone else.
</p>
</div>
<Section>
<Container>
<div className="flex flex-col md:flex-row gap-8 items-center">
<div className="flex-1">
<h2 className="text-2xl font-bold mb-4">Our Origin Story</h2>
<p className="text-muted-foreground mb-4">
Fortura was born in the high-stakes world of Visual Effects (VFX) production. VFX studios operate on razor-thin margins, require massive computational power, and deal with enormous datasets (petabytes) that need to move fast.
</p>
<p className="text-muted-foreground mb-4">
We saw firsthand how the cloud billing model was financially unsustainable for these studios. They were hemorrhaging money on egress fees, rent for virtual machines that sat idle half the time, and paying premium prices for "enterprise" support that often lagged behind community forums.
</p>
<p className="text-muted-foreground">
So, we built a better way. A way to own the hardware, colocate it for low-cost power and bandwidth, and automate the hell out of operations so a small team could manage massive infrastructure.
</p>
</div>
<div className="flex-1 flex justify-center">
{/* Placeholder for an image or graphic */}
<div className="bg-muted border rounded-xl w-full h-64 flex items-center justify-center">
<span className="text-muted-foreground">VFX Studio Image/Graphic</span>
</div>
</div>
</div>
</Container>
</Section>
<Section background="muted">
<Container>
<h2 className="text-2xl font-bold mb-6 text-center">Why That Matters</h2>
<Grid columns={{ initial: 1, md: 3 }} gap="6">
<Card>
<CardHeader>
<AwardIcon className="size-8 text-primary mb-2" />
<CardTitle>Petabyte Scale</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
We cut our teeth on systems handling hundreds of terabytes to petabytes of data. This means we know how to design for scale, performance, and cost-efficiency from the ground up.
</CardDescription>
</CardContent>
</Card>
<Card>
<CardHeader>
<UsersIcon className="size-8 text-primary mb-2" />
<CardTitle>Low Budgets</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
VFX studios have tight budgets. Every dollar spent on infrastructure is a dollar not spent on creative talent or compute rendering the next shot. This discipline forces us to find the absolute cheapest, most efficient solutions.
</CardDescription>
</CardContent>
</Card>
<Card>
<CardHeader>
<ZapIcon className="size-8 text-primary mb-2" />
<CardTitle>Real-Time Demands</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
In VFX, deadlines are absolute. A render farm that's down is a studio that's losing money. This means we obsess over uptime, redundancy, and zero-downtime upgrades. What works for 24/7 VFX pipelines works for any business-critical application.
</CardDescription>
</CardContent>
</Card>
</Grid>
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Our Mission</h2>
<p className="text-muted-foreground mb-4 max-w-3xl">
Our mission is to fundamentally change how enterprises think about infrastructure. The cloud model of "rent forever" is a bad deal for anyone with significant scale or data. It's a wealth transfer from innovative businesses to cloud providers.
</p>
<p className="text-muted-foreground mb-6 max-w-3xl">
We show you how to own your infrastructure, colocate it for performance and cost, and operate it with a tiny fraction of the overhead of a cloud provider. You keep the performance, you keep the data, and you keep the savings.
</p>
<Callout variant="default">
<span className="font-semibold">"Your hosting provider is fucking you."</span> This isn't just a tagline; it's a technical reality. We're here to help you take control.
</Callout>
</Container>
</Section>
<Section background="muted" className="text-center">
<Container>
<h2 className="text-2xl font-bold mb-4">Ready to take control?</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Book a free architecture call to see how much you can save by moving from renting to owning.
</p>
<Button asChild size="lg">
<a href="/contact">
Book Architecture Call
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</Container>
</Section>
</Container>
</Section>
<Footer />
</div>
);
}

View File

@ -0,0 +1,4 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source);

View File

@ -0,0 +1,175 @@
// app/contact/page.tsx
'use client';
import * as React from 'react';
import { ExternalLinkIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section } from '@workspace/ui/components/layout';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { Input } from '@workspace/ui/components/ui';
import { Textarea } from '@workspace/ui/components/ui';
import { Label } from '@workspace/ui/components/ui';
export default function ContactPage() {
// State for form inputs
const [name, setName] = React.useState('');
const [email, setEmail] = React.useState('');
const [company, setCompany] = React.useState('');
const [message, setMessage] = React.useState('');
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [submitSuccess, setSubmitSuccess] = React.useState(false);
const [submitError, setSubmitError] = React.useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitError('');
setSubmitSuccess(false);
// Basic validation
if (!name || !email || !message) {
setSubmitError('Please fill in all required fields.');
setIsSubmitting(false);
return;
}
// Mock submission logic (replace with actual API call)
setTimeout(() => {
console.log({ name, email, company, message });
// Simulate success
setSubmitSuccess(true);
setIsSubmitting(false);
// Reset form
setName('');
setEmail('');
setCompany('');
setMessage('');
}, 1000);
};
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Contact Us</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
Book a free architecture call, request a cost estimate, or ask a question.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-6xl mx-auto">
{/* Contact Form */}
<Card>
<CardHeader>
<CardTitle>Send us a message</CardTitle>
<CardDescription>
Fill out the form and we'll get back to you as soon as possible.
</CardDescription>
</CardHeader>
<CardContent>
{submitSuccess ? (
<div className="bg-green-500/10 border border-green-500/30 rounded-md p-4 text-green-500">
<p className="font-semibold">Message Sent!</p>
<p>Thank you for reaching out. We'll get back to you soon.</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="name">Name *</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="email">Email *</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="company">Company</Label>
<Input
id="company"
value={company}
onChange={(e) => setCompany(e.target.value)}
/>
</div>
<div>
<Label htmlFor="message">Message *</Label>
<Textarea
id="message"
rows={5}
value={message}
onChange={(e) => setMessage(e.target.value)}
required
/>
</div>
{submitError && (
<div className="text-red-500 text-sm">{submitError}</div>
)}
<Button type="submit" disabled={isSubmitting} className="w-full">
{isSubmitting ? 'Sending...' : 'Send Message'}
</Button>
</form>
)}
</CardContent>
</Card>
{/* Calendly Embed and Info */}
<div>
<Card className="mb-6">
<CardHeader>
<CardTitle>Book an Architecture Call</CardTitle>
<CardDescription>
30-minute consultation to discuss your infrastructure and potential savings.
</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4 text-muted-foreground">
During this call, we'll review your current setup, identify cost-cutting opportunities, and outline a migration path.
</p>
{/* Calendly Inline Embed */}
<div className="bg-muted rounded-lg p-4 text-center">
<p className="text-muted-foreground">Calendly Embed Placeholder</p>
<p className="text-sm mt-2">(This would be a real Calendly widget)</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Security Inquiries</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4 text-muted-foreground">
For sensitive security-related questions, please use our dedicated channel.
</p>
<Button asChild variant="outline" className="w-full">
<a href="mailto:security@fortura.ai">
security@fortura.ai
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</CardContent>
</Card>
</div>
</div>
</Container>
</Section>
<Footer />
</div>
);
}

View File

@ -0,0 +1,135 @@
import { source } from '@/lib/source';
import {
DocsPage,
DocsBody,
DocsDescription,
DocsTitle,
EditOnGitHub,
} from 'fumadocs-ui/page';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import { notFound } from 'next/navigation';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { ComponentPreview } from '@/components/docs/component-preview';
import { ComponentInstallation } from '@/components/docs/component-installation';
import { ExternalLink } from '@/components/docs/external-link';
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Footer } from '@workspace/ui/components/docs/footer';
import {
CodeBlock,
type CodeBlockProps,
Pre,
} from '@/components/docs/codeblock';
import { DocsAuthor } from '@/components/docs/docs-author';
import { DocsBreadcrumb } from '@/components/docs/docs-breadcrumb';
export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDX = page.data.body;
return (
<DocsPage
toc={page.data.toc}
full={page.data.full}
footer={{ component: <Footer /> }}
tableOfContent={{ style: 'clerk' }}
>
<DocsBreadcrumb slug={params.slug} />
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="-my-1.5">
{page.data.description}
</DocsDescription>
{page.data.author && (
<DocsAuthor name={page.data.author.name} url={page.data.author?.url} />
)}
<div className="flex flex-row gap-2 items-center">
<EditOnGitHub
className="border-0"
href={`https://github.com/animate-ui/animate-ui/blob/main/apps/www/content/docs/${params.slug ? `${params.slug.join('/')}.mdx` : 'index.mdx'}`}
/>
</div>
<DocsBody>
<MDX
components={{
...defaultMdxComponents,
ComponentPreview,
ComponentInstallation,
TypeTable,
ExternalLink,
Steps,
Step,
pre: (props: CodeBlockProps) => (
<CodeBlock {...props} className="">
<Pre>{props.children}</Pre>
</CodeBlock>
),
}}
/>
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
authors: page.data?.author
? [
{
name: page.data.author.name,
...(page.data.author?.url && { url: page.data.author.url }),
},
]
: {
name: 'imskyleen',
url: 'https://github.com/imskyleen',
},
openGraph: {
title: page.data.title,
description: page.data.description,
url: 'https://animate-ui.com',
siteName: 'Animate UI',
images: [
{
url: 'https://animate-ui.com/og-image.png',
width: 1200,
height: 630,
alt: 'Animate UI',
},
],
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
site: '@animate_ui',
title: page.data.title,
description: page.data.description,
images: [
{
url: 'https://animate-ui.com/og-image.png',
width: 1200,
height: 630,
alt: 'Animate UI',
},
],
},
};
}

View File

@ -0,0 +1,29 @@
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import type { ReactNode } from 'react';
import { baseOptions } from '@/app/layout.config';
import { source } from '@/lib/source';
import { ThemeSwitcher } from '@/components/theme-switcher';
import XIcon from '@workspace/ui/components/icons/x-icon';
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout
githubUrl="https://github.com/animate-ui/animate-ui"
links={[
{
icon: <XIcon />,
url: 'https://x.com/animate_ui',
text: 'X',
type: 'icon',
},
]}
tree={source.pageTree}
themeSwitch={{
component: <ThemeSwitcher />,
}}
{...baseOptions}
>
{children}
</DocsLayout>
);
}

View File

@ -0,0 +1,9 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
robots: 'noindex,nofollow',
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@ -0,0 +1,5 @@
import { RadixSidebarDemo } from '@/registry/demo/radix/sidebar';
const RadixSidebarDemoPage = () => <RadixSidebarDemo />;
export default RadixSidebarDemoPage;

30
apps/www/app/faq/page.tsx Normal file
View File

@ -0,0 +1,30 @@
// app/faq/page.tsx
'use client';
import * as React from 'react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section } from '@workspace/ui/components/layout';
import { FAQAccordion } from '@workspace/ui/components/interactive';
export default function FAQPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Frequently Asked Questions</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
Answers to common questions about Fortura's approach, process, and services.
</p>
</div>
<FAQAccordion />
</Container>
</Section>
<Footer />
</div>
);
}

BIN
apps/www/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,8 @@
import { Logo } from '@/components/logo';
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
export const baseOptions: BaseLayoutProps = {
nav: {
title: <Logo containerClassName="md:mt-0.5 md:mb-2.5" size="sm" betaTag />,
},
};

55
apps/www/app/layout.tsx Normal file
View File

@ -0,0 +1,55 @@
import { RootProvider } from 'fumadocs-ui/provider';
import { Inter } from 'next/font/google';
import type { Metadata } from 'next';
import '@workspace/ui/globals.css';
import { Providers } from './providers';
// import { jsonLd } from '@/lib/json-ld'; // Commented out as it's Animate UI specific
// --- Update metadata for Fortura ---
export const metadata: Metadata = {
title: {
template: '%s - Fortura Data Solutions',
default: 'Fortura Data Solutions - Own Your Infrastructure, Slash Your Bill',
},
description:
'Fortura helps you cut cloud costs by moving to owned, colocated infrastructure. Performance parity with zero vendor lock-in.',
keywords: [
'Fortura',
'Cloud Alternatives',
'Self-hosted',
'Colocation',
'Cloud Cost Reduction',
'Data Ownership',
'Enterprise Infrastructure',
],
// Icons, authors, openGraph, twitter should be updated for Fortura
// For now, keeping minimal changes. In practice, update these sections.
};
const inter = Inter({
subsets: ['latin'],
});
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<head>
{/* <script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/> */}
{/* Add Fortura-specific SEO/script tags here if needed */}
</head>
<body className="flex flex-col min-h-dvh">
{/* Theme provider is handled by RootProvider, but we need our custom providers like ToneProvider */}
{/* Assuming RootProvider handles dark mode, we can nest our providers inside */}
<RootProvider theme={{ defaultTheme: 'dark' }}>
<Providers>
{children}
</Providers>
</RootProvider>
</body>
</html>
);
}

26
apps/www/app/manifest.ts Normal file
View File

@ -0,0 +1,26 @@
import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Animate UI',
short_name: 'Animate UI',
description:
'Fully animated, open-source component distribution built with React, TypeScript, Tailwind CSS, Motion and Shadcn CLI. Browse a list of components you can install, modify, and use in your projects.',
start_url: '/',
display: 'standalone',
background_color: '#fff',
theme_color: '#fff',
icons: [
{
src: '/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png',
},
],
};
}

View File

@ -0,0 +1,30 @@
// app/pricing/page.tsx
'use client';
import * as React from 'react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section } from '@workspace/ui/components/layout';
import { CostCalculator } from '@workspace/ui/components/interactive';
export default function PricingPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Pricing</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
Calculate your Total Cost of Ownership (TCO) and see how much you can save by moving from renting to owning your infrastructure.
</p>
</div>
<CostCalculator />
</Container>
</Section>
<Footer />
</div>
);
}

View File

@ -0,0 +1,101 @@
// app/process/page.tsx
'use client';
import * as React from 'react';
import { MoveUpRightIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { Steps } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
// Mock data for Process Steps (full 6 steps)
const PROCESS_STEPS = [
{
title: "Discovery",
description: "We deep-dive into your current infrastructure, workloads, and pain points.",
youDo: "Share access to cloud billing, architecture docs, and team interviews.",
weDo: "Analyze costs, identify waste, and map technical dependencies."
},
{
title: "Shadow & Map",
description: "We observe how your teams actually work and map desire paths.",
youDo: "Participate in workflow observations and workshops.",
weDo: "Document real workflows, identify automation opportunities, and draft the future state."
},
{
title: "Architect",
description: "We design a tailored, owned infrastructure solution.",
youDo: "Review and approve the proposed architecture and migration plan.",
weDo: "Specify hardware, select software stack, and design for performance and cost."
},
{
title: "Pilot",
description: "We build and test a critical component in the new environment.",
youDo: "Provide feedback on performance, usability, and integration.",
weDo: "Deploy, test, and iterate on the pilot component with your team."
},
{
title: "Migrate",
description: "We execute the planned migration with zero downtime.",
youDo: "Validate functionality and performance post-migration.",
weDo: "Cutover workloads, decommission old systems, and optimize the new stack."
},
{
title: "Operate",
description: "We manage the infrastructure, you focus on your core business.",
youDo: "Use the system and provide ongoing feedback for improvements.",
weDo: "Monitor, maintain, upgrade, and support the infrastructure 24/7."
}
];
export default function ProcessPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Our Process</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
A proven 6-step approach to migrate from cloud rent to owned infrastructure with minimal retraining and maximum savings.
</p>
</div>
<Steps steps={PROCESS_STEPS} />
{/* Embedding Card */}
<Section background="muted" className="mt-16">
<Container>
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<span>Embedding</span>
<MoveUpRightIcon className="ml-2 size-5 text-primary" />
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
We sit with your teams, map real workflows, then automate the boring.
</CardDescription>
<p className="mt-2 text-sm">
Our "Desire Paths" methodology ensures the system we build fits how your team actually works, not how a generic cloud platform prescribes. This minimizes retraining and maximizes adoption.
</p>
<div className="mt-4">
<Button variant="outline" asChild>
<a href="/contact">Book Discovery Call</a>
</Button>
</div>
</CardContent>
</Card>
</Container>
</Section>
</Container>
</Section>
<Footer />
</div>
);
}

View File

@ -0,0 +1,11 @@
import * as React from 'react';
import { ToneProvider } from '@workspace/ui/hooks/use-tone';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ToneProvider>
{children}
</ToneProvider>
);
}

11
apps/www/app/robots.ts Normal file
View File

@ -0,0 +1,11 @@
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: 'https://animate-ui.com/sitemap.xml',
};
}

42
apps/www/app/sitemap.ts Normal file
View File

@ -0,0 +1,42 @@
import type { MetadataRoute } from 'next';
import { source } from '@/lib/source';
import { getGithubLastEdit } from 'fumadocs-core/server';
export const dynamic = 'force-dynamic';
export const revalidate = false;
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const url = (path: string): string =>
new URL(path, 'https://animate-ui.com').toString();
return [
{
url: url('/'),
changeFrequency: 'monthly',
priority: 1,
},
{
url: url('/docs'),
changeFrequency: 'monthly',
priority: 0.8,
},
...(await Promise.all(
source.getPages().map(async (page) => {
const time = await getGithubLastEdit({
owner: 'imskyleen',
repo: 'animate-ui',
path: `content/docs/${page.file.path}`,
});
return {
url: url(page.url),
lastModified: time ? new Date(time) : undefined,
changeFrequency: 'weekly',
priority: 0.5,
} as MetadataRoute.Sitemap[number];
}),
)),
];
}

View File

@ -0,0 +1,256 @@
// app/solutions/[industry]/page.tsx
'use client';
import * as React from 'react';
import { notFound } from 'next/navigation';
import { MoveUpRightIcon, DownloadIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { FeatureGrid } from '@workspace/ui/components/content';
import { Callout } from '@workspace/ui/components/content';
import { Badge } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
// Mock data for industries and their specific details
// In a real app, this would likely come from a CMS or database
const INDUSTRY_DATA: Record<string, any> = {
technology: {
name: "Technology",
title: "Cloud Alternatives for Technology Companies",
description: "From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax.",
painPoints: [
"High and unpredictable cloud bills that scale with user growth.",
"Vendor lock-in making it hard to switch providers or adopt hybrid models.",
"Complexity and overhead of managing cloud-native architectures.",
"Need for high performance and low latency for user-facing applications."
],
proposedStack: [
{
icon: <PlaceholderIcon className="size-5" />,
title: "Kubernetes (K3s/RKE2)",
description: "Lightweight, certified Kubernetes for efficient container orchestration."
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "PostgreSQL/MySQL",
description: "Self-hosted, high-availability databases with point-in-time recovery."
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "MinIO",
description: "S3-compatible object storage for unstructured data."
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "Nextcloud",
description: "Private file sync, sharing, and collaboration platform."
}
],
migrationPlan: [
"Audit current cloud spend and architecture.",
"Design a colocated Kubernetes cluster tailored to your workloads.",
"Migrate stateful services with zero downtime using blue-green deployments.",
"Implement CI/CD pipelines for automated testing and deployment.",
"Optimize for cost and performance post-migration."
],
opsModel: "We provide 24/7 monitoring, managed upgrades, and direct access to our SREs. You retain control over application configuration and scaling decisions.",
outcomes: [
{
metric: "60%",
description: "Reduction in monthly infrastructure costs."
},
{
metric: "Sub-10ms",
description: "Improved database query latency."
},
{
metric: "Zero",
description: "Vendor lock-in with portable, open-source tools."
}
]
},
legal: {
name: "Legal",
title: "Secure Infrastructure for Legal Firms",
description: "Secure, private document management and collaboration environments with strict access controls.",
painPoints: [
"Stringent data privacy regulations (GDPR, CCPA) requiring data residency and control.",
"High cost of secure cloud storage and collaboration tools.",
"Risk of data breaches from third-party SaaS providers.",
"Need for comprehensive audit trails and access logging."
],
proposedStack: [
{
icon: <PlaceholderIcon className="size-5" />,
title: "Nextcloud",
description: "Enterprise-grade file sync, sharing, and collaboration with full audit capabilities."
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "OnlyOffice",
description: "Private, self-hosted office suite for document editing and collaboration."
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "Keycloak",
description: "Centralized identity management with SAML/OIDC integration for existing directories."
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "OpenSearch",
description: "Self-hosted search and analytics engine for document discovery."
}
],
migrationPlan: [
"Inventory and classify all documents and data flows.",
"Implement a secure, private Nextcloud instance with custom compliance policies.",
"Migrate existing document repositories with full version history.",
"Integrate with existing case management and time-tracking systems.",
"Train staff on new tools and security protocols."
],
opsModel: "We manage the underlying infrastructure, security patches, and backups. Your team manages user accounts, permissions, and document workflows.",
outcomes: [
{
metric: "100%",
description: "Data sovereignty and compliance with legal regulations."
},
{
metric: "$50k/year",
description: "Savings vs. proprietary legal tech SaaS bundles."
},
{
metric: "Zero",
description: "Third-party access to sensitive client data."
}
]
},
// Add more industries here...
};
interface SolutionPageProps {
params: { industry: string };
}
export default function SolutionPage({ params }: SolutionPageProps) {
const { industry } = params;
const solution = INDUSTRY_DATA[industry];
if (!solution) {
notFound();
}
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="mb-8">
<Button variant="link" asChild className="px-0">
<a href="/solutions">&larr; All Solutions</a>
</Button>
</div>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">{solution.title}</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
{solution.description}
</p>
</div>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Key Pain Points</h2>
<ul className="space-y-2">
{solution.painPoints.map((point: string, index: number) => (
<li key={index} className="flex items-start">
<span className="mr-2 text-primary"></span>
<span>{point}</span>
</li>
))}
</ul>
</Container>
</Section>
<Section background="muted">
<Container>
<h2 className="text-2xl font-bold mb-6">Proposed Stack</h2>
<FeatureGrid features={solution.proposedStack} columns={{ initial: 1, sm: 2 }} />
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Migration Plan</h2>
<ol className="space-y-4">
{solution.migrationPlan.map((step: string, index: number) => (
<li key={index} className="flex items-start">
<span className="mr-3 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-bold">
{index + 1}
</span>
<span>{step}</span>
</li>
))}
</ol>
</Container>
</Section>
<Section background="muted">
<Container>
<h2 className="text-2xl font-bold mb-6">Operations Model</h2>
<p className="text-muted-foreground">
{solution.opsModel}
</p>
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Expected Outcomes</h2>
<Grid columns={{ initial: 1, sm: 3 }} gap="6">
{solution.outcomes.map((outcome: { metric: string; description: string }, index: number) => (
<Card key={index}>
<CardHeader>
<CardTitle className="text-3xl font-bold">{outcome.metric}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{outcome.description}</CardDescription>
</CardContent>
</Card>
))}
</Grid>
</Container>
</Section>
<Section background="muted" className="text-center">
<Container>
<h2 className="text-2xl font-bold mb-4">Ready to transform your infrastructure?</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Download our industry-specific blueprint or book a free architecture call.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button asChild>
<a href={`/download-blueprint/${industry}`}>
<DownloadIcon className="mr-2 size-4" />
Download Blueprint
</a>
</Button>
<Button asChild variant="outline">
<a href="/contact">
Book Architecture Call
<MoveUpRightIcon className="ml-2 size-4" />
</a>
</Button>
</div>
</Container>
</Section>
</Container>
</Section>
<Footer />
</div>
);
}

View File

@ -0,0 +1,79 @@
// app/solutions/page.tsx
'use client';
import * as React from 'react';
import { ExternalLinkIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { FeatureGrid } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
// Mock data for Industries
const INDUSTRIES = [
{
icon: <PlaceholderIcon className="size-6" />,
title: "Technology",
description: "From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Legal",
description: "Secure, private document management and collaboration environments with strict access controls."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Medical",
description: "HIPAA-compliant infrastructure for storing, processing, and analyzing sensitive patient data."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Media",
description: "High-bandwidth, low-latency solutions for content creation, storage, and distribution."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Research",
description: "Powerful compute and storage for data-intensive scientific research and simulations."
}
];
export default function SolutionsPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Solutions by Industry</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
We tailor our approach to the unique challenges and requirements of your vertical.
</p>
</div>
<FeatureGrid features={INDUSTRIES} columns={{ initial: 1, sm: 2, md: 3 }} />
<Section background="muted" className="text-center mt-16">
<Container>
<h2 className="text-2xl font-bold mb-4">Don't see your industry?</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Our core principles apply broadly. Contact us to discuss how we can adapt our approach for your specific use case.
</p>
<Button asChild>
<a href="/contact">
Contact Us
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</Container>
</Section>
</Container>
</Section>
<Footer />
</div>
);
}

171
apps/www/app/stack/page.tsx Normal file
View File

@ -0,0 +1,171 @@
// app/stack/page.tsx
'use client';
import * as React from 'react';
import { ExternalLinkIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { FeatureGrid } from '@workspace/ui/components/content';
import { Callout } from '@workspace/ui/components/content';
import { Badge } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
// Mock data for Core Frameworks
const CORE_FRAMEWORKS = [
{
icon: <PlaceholderIcon className="size-6" />,
title: "Nextcloud",
description: "Your self-hosted productivity platform. Files, collaboration, office suite, and more."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Seafile",
description: "High-performance file sync and sharing with robust access controls."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "n8n",
description: "Workflow automation. Connect your tools and data with low-code automation."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Keycloak",
description: "Identity and Access Management (IAM). Single Sign-On (SSO) for all your apps."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "PostgreSQL",
description: "The world's most advanced open-source relational database."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "MinIO",
description: "High-performance, S3-compatible object storage."
}
];
// Mock data for AI-Native Stack
const AI_NATIVE_STACK = [
{
icon: <PlaceholderIcon className="size-6" />,
title: "Local LLM Inference",
description: "Run large language models on your own hardware for complete privacy."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "RAG Engine",
description: "Retrieval-Augmented Generation over your private data collections."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Vector DB",
description: "Store and query embeddings for semantic search and AI applications."
}
];
// Mock data for Integrations
const INTEGRATIONS = [
{
icon: <PlaceholderIcon className="size-6" />,
title: "SSO (SAML, OIDC)",
description: "Seamless single sign-on with existing identity providers."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "LDAP",
description: "Integrate with existing directory services for user management."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Jira",
description: "Link issues, automate workflows, and sync data with Jira."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Slack",
description: "Send notifications, trigger workflows, and collaborate in Slack."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Git",
description: "Self-host Git repositories and integrate with CI/CD pipelines."
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "API Connectors",
description: "Pre-built connectors for hundreds of SaaS and enterprise APIs."
}
];
export default function StackPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Our Stack</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
We build on proven, open-source foundations. No vendor lock-in, no proprietary black boxes.
</p>
</div>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Core Frameworks</h2>
<FeatureGrid features={CORE_FRAMEWORKS} columns={{ initial: 1, sm: 2, md: 3 }} />
</Container>
</Section>
<Section background="muted">
<Container>
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
<h2 className="text-2xl font-bold">AI-Native</h2>
<Badge variant="info">Private & Secure</Badge>
</div>
<p className="mb-6 text-muted-foreground max-w-3xl">
Leverage AI on your terms. All inference happens on hardware you control, over data you own. Zero leakage to third parties.
</p>
<FeatureGrid features={AI_NATIVE_STACK} columns={{ initial: 1, md: 3 }} />
<Callout variant="info" className="mt-8">
<span className="font-semibold">AI Compliance:</span> Our local AI stack ensures strict adherence to data privacy regulations (GDPR, HIPAA) and eliminates risks associated with sending sensitive data to external LLM providers.
</Callout>
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Integrations</h2>
<p className="mb-6 text-muted-foreground max-w-3xl">
We wire your new stack into your existing ecosystem. Connect to SSO, directories, project management, communication tools, and more.
</p>
<FeatureGrid features={INTEGRATIONS} columns={{ initial: 1, sm: 2, md: 3 }} />
</Container>
</Section>
<Section background="muted" className="text-center">
<Container>
<h2 className="text-2xl font-bold mb-4">Want to see a specific tool?</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Our stack is flexible. If you have a specific open-source tool in mind, we can likely integrate it.
</p>
<Button asChild>
<a href="/contact">
Discuss Your Stack
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</Container>
</Section>
</Container>
</Section>
<Footer />
</div>
);
}

20
apps/www/components.json Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@workspace/ui/components",
"utils": "@workspace/ui/lib/utils",
"ui": "@workspace/ui/components",
"lib": "@workspace/ui/lib",
"hooks": "@workspace/ui/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -0,0 +1,191 @@
'use client';
import * as React from 'react';
import { AnimatePresence, type HTMLMotionProps, motion } from 'motion/react';
import { FileIcon, FolderIcon, FolderOpenIcon } from 'lucide-react';
import { cn } from '@workspace/ui/lib/utils';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionItemProps,
AccordionTrigger,
AccordionTriggerProps,
useAccordionItem,
} from '@/registry/radix/accordion';
import {
MotionHighlight,
MotionHighlightItem,
} from '@/registry/effects/motion-highlight';
interface FileButtonProps extends HTMLMotionProps<'div'> {
icons?: {
close: React.ReactNode;
open: React.ReactNode;
};
icon?: React.ReactNode;
open?: boolean;
layoutId?: string;
}
const FileButton = React.forwardRef<HTMLDivElement, FileButtonProps>(
({ children, icons, icon, open, layoutId, ...props }, ref) => {
return (
<MotionHighlightItem className="size-full">
<motion.div
ref={ref}
className="flex [&_svg]:size-4 items-center gap-2 p-2 h-10 relative z-10 rounded-lg w-full cursor-default"
{...props}
layoutId={layoutId}
>
{icon
? typeof icon !== 'string'
? icon
: null
: icons && (
<AnimatePresence mode="wait">
<motion.span
key={open ? 'open' : 'close'}
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
exit={{ scale: 0.9 }}
transition={{ duration: 0.15 }}
>
{open
? typeof icons.open !== 'string'
? icons.open
: null
: typeof icons.close !== 'string'
? icons.close
: null}
</motion.span>
</AnimatePresence>
)}
<motion.span className="text-sm block truncate">
{children}
</motion.span>
</motion.div>
</MotionHighlightItem>
);
},
);
FileButton.displayName = 'FileButton';
type FilesProps = React.HTMLAttributes<HTMLDivElement> & {
defaultOpen?: string[];
open?: string[];
onOpenChange?: (open: string[]) => void;
};
const Files = React.forwardRef<HTMLDivElement, FilesProps>(
({ children, className, defaultOpen, open, onOpenChange, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
'relative size-full rounded-xl border bg-background overflow-auto',
className,
)}
{...props}
>
<MotionHighlight
controlledItems
mode="parent"
hover
className="bg-muted rounded-lg pointer-events-none"
>
<Accordion
type="multiple"
className="p-2"
defaultValue={defaultOpen}
value={open}
onValueChange={onOpenChange}
>
{children}
</Accordion>
</MotionHighlight>
</div>
);
},
);
Files.displayName = 'Files';
type FolderTriggerProps = AccordionTriggerProps & {
layoutId?: string;
};
const FolderTrigger = React.forwardRef<HTMLButtonElement, FolderTriggerProps>(
({ children, layoutId, ...props }, ref) => {
const { isOpen } = useAccordionItem();
return (
<AccordionTrigger
ref={ref}
className="h-auto py-0 hover:no-underline font-normal relative z-10"
{...props}
chevron={false}
>
<FileButton
open={isOpen}
icons={{ open: <FolderOpenIcon />, close: <FolderIcon /> }}
layoutId={layoutId}
>
{children}
</FileButton>
</AccordionTrigger>
);
},
);
FolderTrigger.displayName = 'FolderTrigger';
type FolderProps = Omit<AccordionItemProps, 'value'> & {
name: string;
open?: string[];
onOpenChange?: (open: string[]) => void;
defaultOpen?: string[];
layoutId?: string;
};
const Folder = React.forwardRef<HTMLDivElement, FolderProps>(
(
{ children, name, open, defaultOpen, onOpenChange, layoutId, ...props },
ref,
) => (
<AccordionItem
ref={ref}
value={name}
className="relative border-b-0"
{...props}
>
<FolderTrigger layoutId={layoutId}>{name}</FolderTrigger>
<AccordionContent className="relative pb-0 !ml-7 before:absolute before:-left-3 before:inset-y-0 before:w-px before:h-full before:bg-border">
<Accordion
type="multiple"
defaultValue={defaultOpen}
value={open}
onValueChange={onOpenChange}
>
{children}
</Accordion>
</AccordionContent>
</AccordionItem>
),
);
Folder.displayName = 'Folder';
type FileProps = Omit<HTMLMotionProps<'div'>, 'children'> & {
name: string;
layoutId?: string;
};
const File = React.forwardRef<HTMLDivElement, FileProps>(
({ name, layoutId, ...props }, ref) => (
<FileButton ref={ref} icon={<FileIcon />} layoutId={layoutId} {...props}>
{name}
</FileButton>
),
);
File.displayName = 'File';
export { Files, Folder, File };

View File

@ -0,0 +1,70 @@
import {
MotionHighlight,
MotionHighlightItem,
} from '@/registry/effects/motion-highlight';
import { MotionEffect } from '@/registry/effects/motion-effect';
import { Blocks, BringToFront, GitPullRequest } from 'lucide-react';
import ShadcnIcon from '@workspace/ui/components/icons/shadcn-icon';
const CARDS = [
{
value: '1',
icon: BringToFront,
title: 'Animated Components',
description: 'Beautiful Motion-animated components for dynamic websites.',
},
{
value: '2',
icon: GitPullRequest,
title: 'Open Source',
description:
'A project built for the dev community with the dev community.',
},
{
value: '3',
icon: ShadcnIcon,
title: 'Complementary to Shadcn UI',
description:
'The components are designed to be used with Shadcn UI components.',
},
{
value: '4',
icon: Blocks,
title: 'Component Distribution',
description:
'Install the components in your project and modify them as you wish.',
},
];
export const Cards = () => {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<MotionHighlight hover controlledItems className="rounded-xl">
{CARDS.map((card, index) => (
<MotionEffect
key={card.value}
slide={{
direction: 'down',
}}
fade
zoom
delay={index * 0.1}
inView
>
<MotionHighlightItem data-value={card.value} className="h-full">
<div className="p-4 flex flex-col border rounded-xl cursor-default h-full">
<div className="flex items-center justify-around size-10 rounded-lg bg-blue-500/10 mb-2">
<card.icon className="size-5 text-blue-500" />
</div>
<p className="text-base font-medium mb-1">{card.title}</p>
<p className="text-sm text-muted-foreground">
{card.description}
</p>
</div>
</MotionHighlightItem>
</MotionEffect>
))}
</MotionHighlight>
</div>
);
};

View File

@ -0,0 +1,135 @@
import {
Tabs,
TabsContent,
TabsContents,
TabsList,
TabsTrigger,
} from '@/registry/components/tabs';
import TextIcon from '@workspace/ui/components/icons/text-icon';
import PrimitivesIcon from '@workspace/ui/components/icons/primitives-icon';
import EffectsIcon from '@workspace/ui/components/icons/effects-icon';
import ComponentsIcon from '@workspace/ui/components/icons/components-icon';
import BackgroundIcon from '@workspace/ui/components/icons/background-icon';
import { CodeEditor } from '@/registry/components/code-editor';
import { useState } from 'react';
import { index } from '@/__registry__';
import { SectionWrapper } from './section-wrapper';
import { MotionEffect } from '@/registry/effects/motion-effect';
const TABS = [
{
value: 'effects',
label: 'Effects',
icon: EffectsIcon,
name: 'motion-effect-image-grid-demo',
code: index['motion-effect-image-grid-demo'].files[0].content,
demo: index['motion-effect-image-grid-demo'].component,
},
{
value: 'components',
label: 'Components',
icon: ComponentsIcon,
name: 'cursor-demo',
code: index['cursor-demo'].files[0].content,
demo: index['cursor-demo'].component,
},
{
value: 'primitives',
label: 'Primitives',
icon: PrimitivesIcon,
name: 'radix-accordion-demo',
code: index['radix-accordion-demo'].files[0].content,
demo: index['radix-accordion-demo'].component,
},
{
value: 'text',
label: 'Text',
icon: TextIcon,
name: 'writing-text-demo',
code: index['writing-text-demo'].files[0].content,
demo: index['writing-text-demo'].component,
},
{
value: 'background',
label: 'Background',
icon: BackgroundIcon,
name: 'fireworks-background-demo',
code: index['fireworks-background-demo'].files[0].content,
demo: index['fireworks-background-demo'].component,
},
];
export const ComponentsSection = () => {
const [currentTab, setCurrentTab] = useState(TABS[0]?.value ?? '');
return (
<MotionEffect
slide={{
offset: 200,
}}
fade
inView
delay={0.25}
inViewMargin="-50px"
>
<SectionWrapper
subtitle="Components"
title={
<>
Various <span className="text-blue-500">types</span> of animated
components
</>
}
description="Find all types of animated components on Animate UI. Dynamic backgrounds, primitive components animated with Motion and styled with Shadcn's style, animated text and effects to let you easily animate your own components."
>
<Tabs value={currentTab} onValueChange={setCurrentTab}>
<TabsList
className="bg-transparent gap-10 mb-7 max-w-full h-30 pr-4 overflow-x-auto justify-start"
activeClassName="bg-blue-500/10 shadow-none rounded-lg size-full h-16"
>
{TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="relative aspect-square size-16 flex-col gap-y-4 text-neutral-400 dark:text-neutral-500 transition-colors duration-300 data-[state=active]:text-blue-500"
>
<tab.icon className="size-12" />
<p className="absolute top-[calc(100%+12px)]">{tab.label}</p>
</TabsTrigger>
))}
</TabsList>
<TabsContents className="w-full">
{TABS.map((tab) => (
<TabsContent
key={tab.value}
value={tab.value}
className="flex flex-row gap-x-4 h-[450px]"
>
<div
key={currentTab}
className="relative flex-1 shrink-0 p-4 border rounded-xl h-full flex items-center justify-center"
>
<tab.demo />
</div>
<CodeEditor
lang="tsx"
writing={false}
className="flex-1 w-auto hidden lg:block h-full"
title={`${tab.name}.tsx`}
copyButton
>
{tab.code}
</CodeEditor>
</TabsContent>
))}
<TabsContent value="text">TEST</TabsContent>
<TabsContent value="background">TEST</TabsContent>
</TabsContents>
</Tabs>
</SectionWrapper>
</MotionEffect>
);
};

View File

@ -0,0 +1,108 @@
import { MotionEffect } from '@/registry/effects/motion-effect';
import { SectionWrapper } from './section-wrapper';
import {
AnimatedSpan,
Terminal,
TypingAnimation,
} from '@workspace/ui/components/magicui/terminal';
import { useInView, motion } from 'motion/react';
import { useEffect, useRef, useState } from 'react';
import { File, Files, Folder } from '@/components/animate-ui/files';
export const DistributionSection = () => {
const localRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(localRef, {
once: true,
margin: '50px',
});
const [openStructureFile, setOpenStructureFile] = useState(false);
useEffect(() => {
if (!isInView) return;
const interval = setInterval(() => setOpenStructureFile(true), 6000);
return () => clearInterval(interval);
}, [isInView]);
return (
<MotionEffect
slide={{
offset: 200,
}}
fade
inView
delay={0.25}
inViewMargin="-50px"
>
<SectionWrapper
subtitle="Distribution"
title={
<>
Not a library but a{' '}
<span className="text-emerald-500">component distribution</span>
</>
}
description="Animate UI is not a library, it's a component distribution system. You can use the components in your project simply by installing them with the shadcn/ui CLI, or by copying the code and pasting it into your project."
color="text-emerald-500"
backgroundColor="bg-emerald-500/10 hover:bg-emerald-500/20"
>
<div ref={localRef} className="flex lg:flex-row flex-col gap-4">
{isInView && (
<>
<Terminal className="h-[418px] w-full lg:flex-1 max-w-none">
<TypingAnimation>
&gt; pnpm dlx shadcn@latest add
&quot;https://animate-ui.com/r/counter&quot;
</TypingAnimation>
<AnimatedSpan delay={4000} className="text-green-500">
<span> Checking registry.</span>
</AnimatedSpan>
<AnimatedSpan delay={4500} className="text-green-500">
<span> Installing dependencies.</span>
</AnimatedSpan>
<AnimatedSpan delay={5000} className="text-green-500">
<span> Created 1 file:</span>
</AnimatedSpan>
<AnimatedSpan delay={5500} className="text-green-500">
{!openStructureFile ? (
<motion.span layoutId="counter-file">
&nbsp;&nbsp;- components/animate-ui/counter.tsx
</motion.span>
) : (
<span>&nbsp;&nbsp;- components/animate-ui/counter.tsx</span>
)}
</AnimatedSpan>
</Terminal>
<Files
defaultOpen={['components']}
className="h-[418px] w-full flex-1"
>
<Folder name="app" defaultOpen={['(home)']}>
<File name="layout.tsx" />
<File name="page.tsx" />
<File name="global.css" />
</Folder>
<Folder name="components" defaultOpen={['animate-ui']}>
<Folder name="animate-ui">
<File name="cursor.tsx" />
<File name="tabs.tsx" />
{openStructureFile && (
<File name="counter.tsx" layoutId="counter-file" />
)}
</Folder>
<File name="button.tsx" />
<File name="tabs.tsx" />
<File name="dialog.tsx" />
</Folder>
<File name="package.json" />
</Files>
</>
)}
</div>
</SectionWrapper>
</MotionEffect>
);
};

View File

@ -0,0 +1,134 @@
'use client';
import {
type HTMLAttributes,
type ReactNode,
forwardRef,
useCallback,
useRef,
useState,
} from 'react';
import { cn } from '@workspace/ui/lib/utils';
import {
ScrollArea,
ScrollBar,
ScrollViewport,
} from '@workspace/ui/components/ui/scroll-area';
import type { ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
import { CopyButton } from '@/registry/buttons/copy';
export type CodeBlockProps = HTMLAttributes<HTMLElement> & {
icon?: ReactNode;
allowCopy?: boolean;
viewportProps?: ScrollAreaPrimitive.ScrollAreaViewportProps;
onCopy?: () => void;
};
export const Pre = forwardRef<HTMLPreElement, HTMLAttributes<HTMLPreElement>>(
({ className, ...props }, ref) => {
return (
<pre
ref={ref}
className={cn('p-4 focus-visible:outline-none', className)}
{...props}
>
{props.children}
</pre>
);
},
);
Pre.displayName = 'Pre';
export const CodeBlock = forwardRef<HTMLElement, CodeBlockProps>(
(
{
title,
allowCopy = true,
icon,
viewportProps,
onCopy: onCopyEvent,
...props
},
ref,
) => {
const [isCopied, setIsCopied] = useState(false);
const areaRef = useRef<HTMLDivElement>(null);
const onCopy = useCallback(() => {
const pre = areaRef.current?.getElementsByTagName('pre').item(0);
if (!pre) return;
const clone = pre.cloneNode(true) as HTMLElement;
clone.querySelectorAll('.nd-copy-ignore').forEach((node) => {
node.remove();
});
void navigator.clipboard.writeText(clone.textContent ?? '').then(() => {
setIsCopied(true);
onCopyEvent?.();
setTimeout(() => setIsCopied(false), 3000);
});
}, [onCopyEvent]);
return (
<figure
ref={ref}
{...props}
className={cn(
'not-prose group fd-codeblock relative my-6 overflow-hidden rounded-xl border border-border text-sm [&.shiki]:!bg-muted/50',
props.className,
)}
>
{title ? (
<div className="flex flex-row items-center gap-2 bg-muted border-b border-border/75 dark:border-border/50 px-4 h-10">
{icon ? (
<div
className="text-muted-foreground [&_svg]:size-3.5"
dangerouslySetInnerHTML={
typeof icon === 'string' ? { __html: icon } : undefined
}
>
{typeof icon !== 'string' ? icon : null}
</div>
) : null}
<figcaption className="flex-1 truncate text-muted-foreground">
{title}
</figcaption>
{allowCopy ? (
<CopyButton
size="sm"
variant="ghost"
className="-me-2 bg-transparent hover:bg-black/5 dark:hover:bg-white/10"
onClick={onCopy}
isCopied={isCopied}
/>
) : null}
</div>
) : (
allowCopy && (
<CopyButton
size="sm"
variant="ghost"
className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10"
onClick={onCopy}
isCopied={isCopied}
/>
)
)}
<ScrollArea ref={areaRef} dir="ltr">
<ScrollViewport
{...viewportProps}
className={cn('max-h-[600px]', viewportProps?.className)}
>
{props.children}
</ScrollViewport>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</figure>
);
},
);
CodeBlock.displayName = 'CodeBlock';

View File

@ -0,0 +1,78 @@
'use client';
import { index } from '@/__registry__';
import { cn } from '@workspace/ui/lib/utils';
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
TabsContents,
} from '@/registry/radix/tabs';
import { CodeTabs } from '@/registry/components/code-tabs';
import { ComponentManualInstallation } from './component-manual-installation';
interface ComponentInstallationProps
extends React.HTMLAttributes<HTMLDivElement> {
name: string;
}
export function ComponentInstallation({
name,
className,
...props
}: ComponentInstallationProps) {
const component = index[name];
const commands = {
npm: `npx shadcn@latest add "${component.command}"`,
pnpm: `pnpm dlx shadcn@latest add "${component.command}"`,
yarn: `npx shadcn@latest add "${component.command}"`,
bun: `bun x --bun shadcn@latest add "${component.command}"`,
};
return (
<div
className={cn(
'relative my-4 flex flex-col space-y-2 lg:max-w-[120ch]',
className,
)}
{...props}
>
<Tabs defaultValue="cli" className="relative mr-auto w-full">
<TabsList
className="justify-start mb-2 rounded-xl h-10 bg-transparent p-0"
activeClassName="bg-neutral-100 dark:bg-neutral-800 shadow-none rounded-lg"
>
<TabsTrigger
value="cli"
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
CLI
</TabsTrigger>
<TabsTrigger
value="manual"
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
Manual
</TabsTrigger>
</TabsList>
<TabsContents>
<TabsContent value="cli">
<CodeTabs codes={commands} />
</TabsContent>
<TabsContent value="manual">
<ComponentManualInstallation
path={component.files[0].target}
dependencies={component.dependencies}
devDependencies={component.devDependencies}
registryDependencies={component.registryDependencies}
code={component.files[0].content}
/>
</TabsContent>
</TabsContents>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,130 @@
'use client';
import { DynamicCodeBlock } from '@/components/docs/dynamic-codeblock';
import { CodeTabs } from '@/registry/components/code-tabs';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { CollapsibleContent } from 'fumadocs-ui/components/ui/collapsible';
import { Collapsible } from 'fumadocs-ui/components/ui/collapsible';
import { CollapsibleTrigger } from 'fumadocs-ui/components/ui/collapsible';
import { Button } from '@workspace/ui/components/ui/button';
import { cn } from '@workspace/ui/lib/utils';
import { useRef, useState } from 'react';
import ReactIcon from '@workspace/ui/components/icons/react-icon';
const getDepsCommands = (dependencies?: string[]) => {
if (!dependencies) return undefined;
return {
npm: `npm install ${dependencies?.join(' ')}`,
pnpm: `pnpm add ${dependencies?.join(' ')}`,
yarn: `yarn add ${dependencies?.join(' ')}`,
bun: `bun add ${dependencies?.join(' ')}`,
};
};
const getRegistryDepsCommands = (dependencies?: string[]) => {
if (!dependencies) return undefined;
const quotedDependencies = dependencies.map((dep) => `"${dep}"`).join(' ');
return {
npm: `npx shadcn@latest add ${quotedDependencies}`,
pnpm: `pnpm dlx shadcn@latest add ${quotedDependencies}`,
yarn: `npx shadcn@latest add ${quotedDependencies}`,
bun: `bun x --bun shadcn@latest add ${quotedDependencies}`,
};
};
export const ComponentManualInstallation = ({
path,
dependencies,
devDependencies,
registryDependencies,
code,
}: {
path: string;
dependencies?: string[];
devDependencies?: string[];
registryDependencies?: string[];
code: string;
}) => {
const depsCommands = getDepsCommands(dependencies);
const devDepsCommands = getDepsCommands(devDependencies);
const registryDepsCommands = getRegistryDepsCommands(registryDependencies);
const [isOpened, setIsOpened] = useState(false);
const collapsibleRef = useRef<HTMLDivElement>(null);
return (
<Steps>
{dependencies && depsCommands && (
<Step>
<h4 className="pt-1 pb-4">Install the following dependencies:</h4>
<CodeTabs codes={depsCommands} />
</Step>
)}
{devDependencies && devDepsCommands && (
<Step>
<h4 className="pt-1 pb-4">Install the following dev dependencies:</h4>
<CodeTabs codes={devDepsCommands} />
</Step>
)}
{registryDependencies && registryDepsCommands && (
<Step>
<h4 className="pt-1 pb-4">
Install the following registry dependencies:
</h4>
<CodeTabs codes={registryDepsCommands} />
</Step>
)}
<Step>
<h4 className="pt-1 pb-4">
Copy and paste the following code into your project:
</h4>
<Collapsible open={isOpened} onOpenChange={setIsOpened}>
<div ref={collapsibleRef} className="relative overflow-hidden">
<CollapsibleContent
forceMount
className={cn('overflow-hidden', !isOpened && 'max-h-32')}
>
<div
className={cn(
'[&_pre]:my-0 [&_pre]:max-h-[650px] [&_code]:pb-[60px]',
!isOpened
? '[&_pre]:overflow-hidden'
: '[&_pre]:overflow-auto]',
)}
>
<DynamicCodeBlock
code={code}
lang="tsx"
title={path}
icon={<ReactIcon />}
/>
</div>
</CollapsibleContent>
<div
className={cn(
'absolute flex items-center justify-center bg-gradient-to-b rounded-t-xl from-neutral-300/30 to-white dark:from-neutral-700/30 dark:to-neutral-950 p-2',
isOpened ? 'inset-x-0 bottom-0 h-12' : 'inset-0',
)}
>
<CollapsibleTrigger asChild>
<Button variant="secondary" className="h-8 text-xs">
{isOpened ? 'Collapse' : 'Expand'}
</Button>
</CollapsibleTrigger>
</div>
</div>
</Collapsible>
</Step>
<Step>
<h4 className="pt-1 pb-4">
Update the import paths to match your project setup.
</h4>
</Step>
</Steps>
);
};

View File

@ -0,0 +1,181 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { index } from '@/__registry__';
import { ComponentWrapper } from '@/components/docs/component-wrapper';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
TabsContents,
} from '@/registry/radix/tabs';
import { cn } from '@workspace/ui/lib/utils';
import { Loader } from 'lucide-react';
import { Suspense, useEffect, useMemo, useState } from 'react';
import { DynamicCodeBlock } from '@/components/docs/dynamic-codeblock';
import ReactIcon from '@workspace/ui/components/icons/react-icon';
import { type Binds, Tweakpane } from '@workspace/ui/components/docs/tweakpane';
interface ComponentPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
name: string;
iframe?: boolean;
bigScreen?: boolean;
}
function flattenFirstLevel<T>(input: Record<string, any>): T {
return Object.values(input).reduce((acc, current) => {
return { ...acc, ...current };
}, {} as T);
}
function unwrapValues(obj: Record<string, any>): Record<string, any> {
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
if ('value' in obj) {
return obj.value;
}
const result: Record<string, any> = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = unwrapValues(obj[key]);
}
}
return result;
}
return obj;
}
export function ComponentPreview({
name,
className,
iframe = false,
bigScreen = false,
...props
}: ComponentPreviewProps) {
const [binds, setBinds] = useState<Binds | null>(null);
const [componentProps, setComponentProps] = useState<Record<
string,
unknown
> | null>(null);
const code = useMemo(() => {
const code = index[name]?.files?.[0]?.content;
if (!code) {
console.error(`Component with name "${name}" not found in registry.`);
return null;
}
return code;
}, [name]);
const preview = useMemo(() => {
const Component = index[name]?.component;
if (Object.keys(Component?.demoProps ?? {}).length !== 0) {
if (componentProps === null)
setComponentProps(unwrapValues(Component?.demoProps));
if (binds === null) setBinds(Component?.demoProps);
}
if (!Component) {
console.error(`Component with name "${name}" not found in registry.`);
return (
<p className="text-sm text-muted-foreground">
Component{' '}
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
</code>{' '}
not found in registry.
</p>
);
}
return <Component {...flattenFirstLevel(componentProps ?? {})} />;
}, [name, componentProps, binds]);
useEffect(() => {
if (!binds) return;
setComponentProps(unwrapValues(binds));
}, [binds]);
return (
<div
className={cn(
'relative my-4 flex flex-col space-y-2 lg:max-w-[120ch] not-prose',
className,
)}
{...props}
>
<Tabs defaultValue="preview" className="relative mr-auto w-full">
<div className="flex items-center justify-between pb-2">
<TabsList
className="justify-start rounded-xl h-10 bg-transparent p-0"
activeClassName="bg-neutral-100 dark:bg-neutral-800 shadow-none rounded-lg"
>
<TabsTrigger
value="preview"
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
Preview
</TabsTrigger>
<TabsTrigger
value="code"
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
Code
</TabsTrigger>
</TabsList>
</div>
<TabsContents>
<TabsContent
value="preview"
className="relative rounded-md h-full"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
<ComponentWrapper
name={name}
iframe={iframe}
bigScreen={bigScreen}
tweakpane={
binds && <Tweakpane binds={binds} onBindsChange={setBinds} />
}
>
<Suspense
fallback={
<div className="flex items-center text-sm text-muted-foreground">
<Loader className="mr-2 size-4 animate-spin" />
Loading...
</div>
}
>
{preview}
</Suspense>
</ComponentWrapper>
</TabsContent>
<TabsContent
value="code"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
<div className="flex flex-col space-y-4">
<div className="w-full rounded-md [&_pre]:my-0 [&_pre]:max-h-[400px] [&_pre]:overflow-auto">
<DynamicCodeBlock
code={code}
lang="tsx"
title={`${name}.tsx`}
icon={<ReactIcon />}
/>
</div>
</div>
</TabsContent>
</TabsContents>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,127 @@
'use client';
import { OpenInV0Button } from '@workspace/ui/components/docs/open-in-v0-button';
import { Button } from '@workspace/ui/components/ui/button';
import { cn } from '@workspace/ui/lib/utils';
import { Fullscreen, RotateCcw, SlidersHorizontal } from 'lucide-react';
import { useState } from 'react';
import { motion } from 'motion/react';
import Iframe from './iframe';
import { useIsMobile } from '@workspace/ui/hooks/use-mobile';
interface ComponentWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
name: string;
iframe?: boolean;
bigScreen?: boolean;
tweakpane?: React.ReactNode;
}
export const ComponentWrapper = ({
className,
children,
name,
iframe = false,
bigScreen = false,
tweakpane,
}: ComponentWrapperProps) => {
const [tweakMode, setTweakMode] = useState(false);
const [key, setKey] = useState(0);
const isMobile = useIsMobile();
return (
<motion.div
className={cn(
'max-w-screen relative rounded-xl border bg-background flex flex-col md:flex-row',
bigScreen && 'overflow-hidden',
className,
)}
>
<motion.div className="relative size-full flex-1">
{!iframe && (
<div className="absolute top-3 right-3 z-[9] bg-background flex items-center justify-end gap-2 p-1 rounded-[11px]">
<OpenInV0Button url={`https://animate-ui.com/r/${name}.json`} />
<Button
onClick={() => setKey((prev) => prev + 1)}
className="flex items-center rounded-lg"
variant="neutral"
size="icon-sm"
asChild
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<RotateCcw aria-label="restart-btn" size={14} />
</motion.button>
</Button>
{iframe && (
<Button
onClick={() => window.open(`/examples/${name}`, '_blank')}
className="flex items-center rounded-lg"
variant="neutral"
size="icon-sm"
asChild
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Fullscreen aria-label="fullscreen-btn" size={14} />
</motion.button>
</Button>
)}
{tweakpane && (
<Button
onClick={() => setTweakMode((prev) => !prev)}
className="flex items-center rounded-lg"
variant="neutral"
size="icon-sm"
asChild
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<SlidersHorizontal aria-label="tweak-btn" size={14} />
</motion.button>
</Button>
)}
</div>
)}
{iframe ? (
<Iframe key={key} name={name} bigScreen={bigScreen} />
) : (
<div
key={key}
className="flex min-h-[400px] w-full items-center justify-center px-10 py-16"
>
{children}
</div>
)}
</motion.div>
<motion.div
initial={false}
animate={{
width: isMobile ? '100%' : tweakMode ? '250px' : '0px',
height: isMobile ? (tweakMode ? '250px' : '0px') : 'auto',
opacity: tweakMode ? 1 : 0,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
restDelta: 0.01,
}}
className="relative"
>
<div className="absolute inset-0 overflow-y-auto">{tweakpane}</div>
</motion.div>
</motion.div>
);
};

View File

@ -0,0 +1,32 @@
import { cn } from '@workspace/ui/lib/utils';
interface DocsAuthorProps {
name: string;
url?: string;
}
const nameClassName =
'text-foreground underline underline-offset-2 decoration-blue-500 font-medium';
export const DocsAuthor = ({ name, url }: DocsAuthorProps) => {
return (
<span className={'text-sm text-fd-muted-foreground italic'}>
Made by{' '}
{url ? (
<a
className={cn(
nameClassName,
'cursor-pointer hover:decoration-foreground',
)}
href={url}
target="_blank"
rel="noopener noreferrer"
>
{name}
</a>
) : (
<span className={nameClassName}>{name}</span>
)}
</span>
);
};

View File

@ -0,0 +1,43 @@
import React from 'react';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from '@workspace/ui/components/ui/breadcrumb';
import { cn } from '@workspace/ui/lib/utils';
export const DocsBreadcrumb = ({ slug }: { slug?: string[] }) => {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/docs">Docs</BreadcrumbLink>
</BreadcrumbItem>
{slug &&
slug.length > 0 &&
slug.map((item, index) => (
<React.Fragment key={item}>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink
className={cn(
'capitalize',
index === slug.length - 1 && 'text-foreground',
)}
href={
index === slug.length - 1 ? `/docs/${slug.join('/')}` : `#`
}
>
{item.replace(/-/g, ' ')}
</BreadcrumbLink>
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@ -0,0 +1,66 @@
'use client';
import { CodeBlock, Pre } from '@/components/docs/codeblock';
import type {
HighlightOptionsCommon,
HighlightOptionsThemes,
} from 'fumadocs-core/highlight';
import { useShiki } from 'fumadocs-core/highlight/client';
import { cn } from '@workspace/ui/lib/utils';
const getComponents = ({
title,
icon,
onCopy,
className,
}: {
title?: string;
icon?: React.ReactNode;
onCopy?: () => void;
className?: string;
}) =>
({
pre(props) {
return (
<CodeBlock
{...props}
title={title}
icon={icon}
onCopy={onCopy}
className={cn('my-0', props.className, className)}
>
<Pre>{props.children}</Pre>
</CodeBlock>
);
},
}) satisfies HighlightOptionsCommon['components'];
export function DynamicCodeBlock({
lang,
code,
options,
title,
icon,
onCopy,
className,
}: {
lang: string;
code: string;
title?: string;
icon?: React.ReactNode;
onCopy?: () => void;
options?: Omit<HighlightOptionsCommon, 'lang'> & HighlightOptionsThemes;
className?: string;
}) {
const components = getComponents({ title, icon, onCopy, className });
return useShiki(code, {
lang,
...options,
components: {
...components,
...options?.components,
},
withPrerenderScript: true,
});
}

View File

@ -0,0 +1,26 @@
'use client';
import { ExternalLinkIcon } from 'lucide-react';
import { motion } from 'motion/react';
export const ExternalLink = ({
href,
text,
}: {
href: string;
text: string;
}) => {
return (
<motion.a
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
href={href}
target="_blank"
rel="noopener noreferrer"
className="not-prose w-fit flex flex-row items-center rounded-md bg-muted hover:bg-muted/70 transition pl-3 pr-2.5 py-1 text-sm font-medium text-muted-foreground"
>
<span>{text}</span>
<ExternalLinkIcon className="ml-1.5 h-4 w-4" />
</motion.a>
);
};

View File

@ -0,0 +1,22 @@
import type { IconProps } from '@/registry/icons/icon';
import React from 'react';
export const IconShowcase = ({
icon: Icon,
displayTitle = true,
...props
}: {
icon: React.ComponentType<IconProps<string>>;
displayTitle?: boolean;
} & IconProps<string>) => {
return (
<div className="relative h-[200px] w-full lg:w-[250px] max-w-[250px] mx-auto rounded-2xl aspect-square bg-muted/50 border flex items-center justify-center">
{props.animation && displayTitle ? (
<p className="absolute whitespace-nowrap -top-4.5 py-1.5 px-3 bg-border rounded-b-lg left-1/2 -translate-x-1/2 text-sm text-muted-foreground">
{props.animation}
</p>
) : null}
<Icon animate className="text-current size-[100px]" {...props} />
</div>
);
};

View File

@ -0,0 +1,313 @@
'use client';
import Fuse from 'fuse.js';
import { index } from '@/__registry__';
import { MotionHighlight } from '@/registry/effects/motion-highlight';
import { AnimateIcon, staticAnimations } from '@/registry/icons/icon';
import { X } from '@/registry/icons/x';
import { Input } from '@workspace/ui/components/ui/input';
import { cn } from '@workspace/ui/lib/utils';
import { useEffect, useMemo, useState } from 'react';
import { motion } from 'motion/react';
import { CodeTabs } from '@/registry/components/code-tabs';
import { DynamicCodeBlock } from './dynamic-codeblock';
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
TabsContents,
} from '@/registry/components/tabs';
import ReactIcon from '@workspace/ui/components/icons/react-icon';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/ui/select';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/registry/components/tooltip';
import { Button } from '@workspace/ui/components/ui/button';
import { RotateCcw } from '@/registry/icons/rotate-ccw';
const staticAnimationsLength = Object.keys(staticAnimations).length;
export const Icons = () => {
const [search, setSearch] = useState('');
const [animationKey, setAnimationKey] = useState(0);
const [activeIcon, setActiveIcon] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<string>('cli');
const [isCopied, setIsCopied] = useState(false);
const [activeAnimation, setActiveAnimation] = useState<string>('default');
const icons = Object.values(index).filter((icon) =>
icon.name.endsWith('-icon'),
);
const fuse = useMemo(() => {
return new Fuse(icons, {
keys: ['name', 'keywords'],
threshold: 0.3,
ignoreLocation: true,
});
}, [icons]);
const filteredIcons = useMemo(() => {
const q = search.trim();
if (!q) return icons;
return fuse.search(q).map((result) => result.item);
}, [search, fuse, icons]);
const icon = useMemo(
() => icons.find((icon) => icon.name === activeIcon),
[activeIcon, icons],
);
const iconName = useMemo(
() =>
icon?.name
.replace('-icon', '')
.split('-')
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
.join(''),
[icon],
);
useEffect(() => {
setActiveAnimation('default');
}, [activeIcon]);
return (
<div className="-mt-4.5 text-black dark:text-white">
<p className="text-sm text-muted-foreground">
{filteredIcons.length} icons {search.length ? 'found' : 'available'}
</p>
<Input
placeholder="Search icons"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div>
{filteredIcons.length ? (
<div className="grid lg:grid-cols-11 sm:grid-cols-9 xs:grid-cols-7 grid-cols-5 gap-4 mt-6">
<TooltipProvider>
<MotionHighlight
hover
className="ring-2 ring-blue-500 bg-transparent rounded-lg -z-1"
>
{filteredIcons.map((icon) => {
const animationsLength = Object.keys(
icon?.component?.animations ?? {},
).length;
return (
<Tooltip side="bottom" key={icon.name}>
<TooltipTrigger>
<div>
<AnimateIcon animateOnHover>
<button
data-value={icon.name}
onClick={() => setActiveIcon(icon.name)}
className="relative group flex items-center justify-center size-full aspect-square rounded-lg p-3.5"
>
{icon?.component && (
<icon.component className="text-current size-full" />
)}
<div
className={cn(
'absolute inset-0 bg-muted rounded-lg -z-2 transition-colors duration-200',
activeIcon === icon.name && 'bg-blue-500/20',
)}
/>
<div className="absolute -bottom-2.5 -right-2.5 flex items-center justify-center text-muted-foreground font-medium size-5 bg-background border group-hover:border-blue-500 group-hover:ring group-hover:ring-blue-500 transition-colors duration-200 rounded-full">
<span className="text-[11px] leading-none">
{staticAnimationsLength + animationsLength}
</span>
</div>
</button>
</AnimateIcon>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{icon.name.replace('-icon', '')}</p>
</TooltipContent>
</Tooltip>
);
})}
</MotionHighlight>
</TooltipProvider>
</div>
) : (
<div className="flex items-center justify-center h-[200px]">
<p className="text-sm text-muted-foreground">No icons found</p>
</div>
)}
</div>
<motion.div
className="fixed z-50 w-[325px] right-0 inset-y-12 rounded-l-2xl border-l border-y bg-background shadow-sm p-4"
initial={{ opacity: 0, x: '100%' }}
animate={activeIcon ? { opacity: 1, x: 0 } : { opacity: 0, x: '100%' }}
exit={{ opacity: 0, x: '100%' }}
transition={{ type: 'spring', stiffness: 150, damping: 25 }}
>
<h2 className="text-lg font-medium mt-1.5">
{activeIcon?.replace('-icon', '')}
</h2>
<AnimateIcon animateOnHover>
<button
onClick={() => setActiveIcon(null)}
className="absolute cursor-pointer top-5 right-5 size-8 rounded-full flex items-center justify-center bg-background hover:bg-muted transition-colors duration-200"
>
<X className="size-5 text-neutral-500" />
</button>
</AnimateIcon>
<div className="h-[calc(100%-3.25rem)] overflow-y-auto">
<div className="h-full flex flex-col justify-between gap-y-4">
<div>
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value)}
className="gap-0"
>
<div className="w-full flex justify-between items-center mb-3">
<h3 className="text-base font-medium pt-0 pb-0 mt-0 mb-0">
Installation
</h3>
<TabsList>
<TabsTrigger value="cli" className="w-[70px]">
CLI
</TabsTrigger>
<TabsTrigger value="manual" className="w-[70px]">
Manual
</TabsTrigger>
</TabsList>
</div>
<TabsContents>
<TabsContent value="cli">
<CodeTabs
codes={{
npm: `npx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
pnpm: `pnpm dlx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
yarn: `npx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
bun: `bun x --bun shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
}}
/>
</TabsContent>
<TabsContent value="manual" className="relative group">
{activeIcon && (
<DynamicCodeBlock
code={icon?.files?.[0]?.content}
lang="jsx"
title={`${icon?.name}.tsx`}
icon={<ReactIcon />}
className="max-h-[92px]"
/>
)}
<div
role="button"
onClick={() => {
navigator.clipboard.writeText(
icon?.files?.[0]?.content ?? '',
);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
}}
className="absolute cursor-pointer inset-px top-[41px] rounded-b-[13px] bg-black/20 invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200 flex items-center justify-center"
>
<p className="text-sm font-medium text-white">
{isCopied ? 'Copied' : 'Copy'}
</p>
</div>
</TabsContent>
</TabsContents>
</Tabs>
<h3 className="text-base font-medium mt-4">Usage</h3>
{activeIcon && (
<DynamicCodeBlock
code={`<${iconName} animateOnHover />
// Or use with the AnimateIcon component
<AnimateIcon animateOnHover>
<${iconName} />
</AnimateIcon>`}
lang="jsx"
/>
)}
</div>
<div className="space-y-4">
{activeIcon && (
<>
<div className="relative h-[200px] w-full mx-auto rounded-2xl aspect-square bg-muted/50 border flex items-center justify-center">
{icon?.component && (
<icon.component
key={`${activeAnimation}-${activeIcon}-${animationKey}`}
animate
animation={activeAnimation}
className="text-current size-[100px]"
/>
)}
<AnimateIcon animateOnHover>
<Button
size="icon-sm"
variant="ghost"
className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10 size-6"
onClick={() => setAnimationKey((prev) => prev + 1)}
>
<RotateCcw className="size-3.5" />
</Button>
</AnimateIcon>
</div>
<Select
value={activeAnimation}
onValueChange={(value) => setActiveAnimation(value)}
>
<SelectTrigger className="w-full !h-11 px-1.5 rounded-lg">
<SelectValue placeholder="Select an animation" />
</SelectTrigger>
<SelectContent>
<div className="space-y-1.5 p-0.5">
{Object.keys({
...staticAnimations,
...(icon?.component?.animations ?? {}),
}).map((animation) => (
<SelectItem
key={animation}
value={animation}
className="!h-8 rounded-md px-0 focus:bg-muted"
>
<div className="gap-2 flex items-center">
<div className="size-8 rounded-md p-1.5 bg-muted">
{icon?.component && (
<icon.component className="text-current size-full" />
)}
</div>
<span>{animation}</span>
</div>
</SelectItem>
))}
</div>
</SelectContent>
</Select>
</>
)}
</div>
</div>
</div>
</motion.div>
</div>
);
};

View File

@ -0,0 +1,28 @@
'use client';
import { cn } from '@workspace/ui/lib/utils';
import { useEffect, useState } from 'react';
export default function Iframe({
name,
bigScreen = false,
}: {
name: string;
bigScreen?: boolean;
}) {
const [iframeUrl, setIframeUrl] = useState<string | null>(null);
useEffect(() => {
const origin = window.location.origin;
setIframeUrl(`${origin}/examples/${name}`);
}, [name]);
if (!iframeUrl) return null;
return (
<iframe
src={iframeUrl}
className={cn('h-[500px] rounded-xl', bigScreen && 'w-[1600px]')}
/>
);
}

View File

@ -0,0 +1,795 @@
'use client';
import {
type Frame,
type Frames,
MotionGrid,
} from '@/registry/components/motion-grid';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/registry/components/tooltip';
import { Trash2Icon } from '@/registry/icons/trash-2';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/registry/radix/dropdown-menu';
import { Button } from '@workspace/ui/components/ui/button';
import { Input } from '@workspace/ui/components/ui/input';
import { Label } from '@workspace/ui/components/ui/label';
import { ScrollArea } from '@workspace/ui/components/ui/scroll-area';
import { cn } from '@workspace/ui/lib/utils';
import {
ArrowLeftIcon,
ArrowRightIcon,
CopyIcon,
PlusIcon,
RotateCcwIcon,
SaveIcon,
CheckIcon,
XIcon,
Timer,
SquareRoundCorner,
} from 'lucide-react';
import * as React from 'react';
const GRID_SIZE = [7, 7] as [number, number];
const GRID_SIZE_MAX = 20;
const GRID_SIZE_MIN = 4;
const DEFAULT_DURATION = '200';
const DEFAULT_BORDER_RADIUS = '100';
const DEFAULT_BORDER_RADIUS_UNIT = '%';
const BORDER_RADIUS_UNITS = ['px', 'rem', 'em', '%'];
const formatGridSizeNumber = (value: number) => {
if (value < GRID_SIZE_MIN) return GRID_SIZE_MIN;
if (value > GRID_SIZE_MAX) return GRID_SIZE_MAX;
return Math.round(value);
};
const MyAnimation = ({
name,
value,
selectAnimation,
deleteAnimation,
active,
}: {
name: string;
value: {
gridSize: [number, number];
frames: Frames;
duration: string;
borderRadius: string;
borderRadiusUnit: string;
};
selectAnimation: () => void;
deleteAnimation: () => void;
active: boolean;
}) => {
const [isDeleting, setIsDeleting] = React.useState<boolean>(false);
const [isHovering, setIsHovering] = React.useState<boolean>(false);
return (
<div
onClick={selectAnimation}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className={cn(
'group flex flex-row gap-3 items-center hover:bg-neutral-100 dark:hover:bg-neutral-900 cursor-pointer rounded-xl p-2',
active && 'bg-neutral-100 dark:bg-neutral-900',
)}
>
<div className="flex size-20 shrink-0 aspect-square justify-center items-center">
<MotionGrid
className={cn(
'dark:bg-neutral-900 bg-neutral-100 group-hover:ring-2 group-hover:ring-neutral-200 dark:group-hover:ring-neutral-800 rounded-md p-1.5',
active && 'ring-2 ring-neutral-300 dark:ring-neutral-700',
value.gridSize[0] > value.gridSize[1]
? 'w-20 h-auto'
: 'h-20 w-auto',
Math.max(value.gridSize[0], value.gridSize[1]) > 10
? 'gap-px'
: 'gap-0.5',
)}
cellProps={{
style: {
borderRadius: `${value.borderRadius}${value.borderRadiusUnit}`,
},
}}
duration={Number(value.duration)}
animate={isHovering}
cellClassName="!size-full"
cellActiveClassName="bg-neutral-800 dark:bg-neutral-200"
cellInactiveClassName="bg-neutral-200 dark:bg-neutral-800"
gridSize={value.gridSize}
frames={value.frames}
/>
</div>
<div className="flex flex-row gap-2 justify-between items-center w-full">
<div className="flex flex-col gap-1 text-left">
<Label className="text-sm">{name}</Label>
<p className="text-xs text-neutral-500 mt-0 mb-0">
{value.gridSize[0]}x{value.gridSize[1]} {value.frames.length}{' '}
frames
</p>
</div>
{isDeleting ? (
<div className="flex flex-row gap-2">
<Button
size="icon-xs"
variant="neutral"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsDeleting(false);
}}
>
<XIcon />
</Button>
<Button
size="icon-xs"
variant="destructive"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
deleteAnimation();
setIsDeleting(false);
}}
>
<CheckIcon />
</Button>
</div>
) : (
<Button
size="icon-xs"
variant="neutral"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsDeleting(true);
}}
>
<Trash2Icon />
</Button>
)}
</div>
</div>
);
};
interface Animation {
gridSize: [number, number];
frames: Frames;
duration: string;
borderRadius: string;
borderRadiusUnit: string;
}
export const MotionGridEditor = () => {
const [gridSizeInput, setGridSizeInput] = React.useState<[string, string]>(
GRID_SIZE.map((n) => n.toString()) as [string, string],
);
const [gridSize, setGridSize] = React.useState<[number, number]>(GRID_SIZE);
const [frames, setFrames] = React.useState<Frames>([[]]);
const [activeFrame, setActiveFrame] = React.useState<number>(0);
const [isCopied, setIsCopied] = React.useState<boolean>(false);
const [isSaved, setIsSaved] = React.useState<boolean>(false);
const [animationName, setAnimationName] = React.useState<string>('');
const [selectedAnimation, setSelectedAnimation] = React.useState<
string | null
>(null);
const [animations, setAnimations] = React.useState<Record<string, Animation>>(
{},
);
const [isDrawing, setIsDrawing] = React.useState(false);
const [drawAction, setDrawAction] = React.useState<'add' | 'remove' | null>(
null,
);
const [duration, setDuration] = React.useState<string>(DEFAULT_DURATION);
const [borderRadius, setBorderRadius] = React.useState<string>(
DEFAULT_BORDER_RADIUS,
);
const [borderRadiusUnit, setBorderRadiusUnit] = React.useState<string>('px');
React.useEffect(() => {
const handleUp = () => {
setIsDrawing(false);
setDrawAction(null);
};
window.addEventListener('mouseup', handleUp);
return () => window.removeEventListener('mouseup', handleUp);
}, []);
const normalizeAnimation = (anim: Partial<Animation>): Animation =>
({
...anim,
duration: anim.duration ?? DEFAULT_DURATION,
borderRadius: anim.borderRadius ?? DEFAULT_BORDER_RADIUS,
borderRadiusUnit: anim.borderRadiusUnit ?? DEFAULT_BORDER_RADIUS_UNIT,
}) as Animation;
React.useEffect(() => {
const raw = localStorage.getItem('animations');
const parsed: Record<string, Partial<Animation>> = raw
? JSON.parse(raw)
: {};
const fixed: Record<string, Animation> = Object.fromEntries(
Object.entries(parsed).map(([key, value]) => [
key,
normalizeAnimation(value),
]),
);
setAnimations(fixed);
localStorage.setItem('animations', JSON.stringify(fixed));
}, []);
const activeFrameDots = new Set<number>(
frames[activeFrame]?.map(([x, y]) => y * gridSize[0]! + x) ?? [],
);
const applyDot = (x: number, y: number, action: 'add' | 'remove') => {
setFrames((prev) => {
const clone = [...prev];
const current = clone[activeFrame]!;
const idx = current.findIndex(([px, py]) => px === x && py === y);
if (action === 'add' && idx === -1) {
clone[activeFrame] = [...current, [x, y]];
} else if (action === 'remove' && idx !== -1) {
clone[activeFrame] = [
...current.slice(0, idx),
...current.slice(idx + 1),
];
}
return clone;
});
};
const moveCurrentFrame = (direction: -1 | 1) => {
if (frames.length <= 1) return;
setFrames((prev) => {
const clone = [...prev];
const len = clone.length;
const newIndex = (activeFrame + direction + len) % len;
const [moved] = clone.splice(activeFrame, 1);
clone.splice(newIndex, 0, moved!);
setActiveFrame(newIndex);
return clone;
});
};
const removeDotNotInGrid = () => {
setFrames((prev) =>
prev.map((frame) =>
frame.filter(([x, y]) => x < gridSize[0] && y < gridSize[1]),
),
);
};
const createNewAnimation = () => {
setGridSize(GRID_SIZE);
setGridSizeInput(GRID_SIZE.map((n) => n.toString()) as [string, string]);
setDuration(DEFAULT_DURATION);
setBorderRadius(DEFAULT_BORDER_RADIUS);
setBorderRadiusUnit(DEFAULT_BORDER_RADIUS_UNIT);
setFrames([[]]);
setActiveFrame(0);
setAnimationName('');
setIsSaved(false);
setIsCopied(false);
setSelectedAnimation(null);
};
return (
<div className="grid grid-cols-12 gap-4 lg:h-[506px]">
<div className="lg:col-span-5 col-span-12 border rounded-xl overflow-hidden flex flex-col min-h-0 h-[506px]">
<div className="relative flex flex-row p-4 bg-muted border-b border-border/75 dark:border-border/50">
<Label>My Animations</Label>
<Button
size="icon-xs"
variant="neutral"
className="absolute top-1/2 -translate-y-1/2 right-2"
onClick={createNewAnimation}
>
<PlusIcon />
</Button>
</div>
<div className="flex-1 flex flex-col gap-y-2 justify-between overflow-y-auto p-2">
<div className="flex flex-col gap-y-2">
{Object.entries(animations).map(([name, value]) => (
<MyAnimation
key={name}
name={name}
value={value}
selectAnimation={() => {
setGridSize(value.gridSize);
setFrames(value.frames);
setDuration(value.duration);
setBorderRadius(value.borderRadius);
setBorderRadiusUnit(value.borderRadiusUnit);
setActiveFrame(0);
setAnimationName(name);
setIsSaved(false);
setIsCopied(false);
setGridSizeInput(
value.gridSize.map((n) => n.toString()) as [string, string],
);
setSelectedAnimation(name);
}}
deleteAnimation={() => {
if (selectedAnimation === name) createNewAnimation();
const newAnimations = { ...animations };
delete newAnimations[name];
setAnimations(newAnimations);
localStorage.setItem(
'animations',
JSON.stringify(newAnimations),
);
}}
active={selectedAnimation === name}
/>
))}
</div>
<span className="text-xs text-neutral-400 dark:text-neutral-600 text-center italic">
Animations stored in local storage
</span>
</div>
</div>
<div className="lg:col-span-7 col-span-12 border rounded-xl overflow-hidden">
<div className="relative flex flex-row justify-between items-center gap-2 p-4 bg-muted border-b border-border/75 dark:border-border/50">
<Label className="whitespace-nowrap">
{selectedAnimation ? 'Edit Animation' : 'Create New Animation'}
</Label>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex flex-row gap-1 items-center">
<Input
placeholder="X"
type="number"
className="border-none h-8 text-sm max-w-8 p-0 bg-white text-center dark:bg-neutral-900 shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={gridSizeInput[0]}
onChange={(e) => {
setGridSizeInput((prev) => [e.target.value, prev[1]]);
setGridSize((prev) => [
formatGridSizeNumber(Number(e.target.value)),
prev[1],
]);
}}
onBlur={() => {
setGridSizeInput(
gridSize.map((n) => n.toString()) as [string, string],
);
removeDotNotInGrid();
}}
/>
<XIcon className="size-3 text-neutral-500 stroke-3" />
<Input
placeholder="Y"
type="number"
className="border-none h-8 text-sm max-w-8 p-0 bg-white text-center dark:bg-neutral-900 shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={gridSizeInput[1]}
onChange={(e) => {
setGridSizeInput((prev) => [prev[0], e.target.value]);
setGridSize((prev) => [
prev[0],
formatGridSizeNumber(Number(e.target.value)),
]);
}}
onBlur={() => {
setGridSizeInput(
gridSize.map((n) => n.toString()) as [string, string],
);
removeDotNotInGrid();
}}
/>
</div>
</div>
<div>
<div className="p-4 relative">
<div className="flex size-20 shrink-0 aspect-square justify-end items-start absolute right-4 top-4">
<MotionGrid
className={cn(
'dark:bg-neutral-900 bg-neutral-100 rounded-md p-1.5',
gridSize[0] > gridSize[1] ? 'w-20 h-auto' : 'h-20 w-auto',
Math.max(gridSize[0], gridSize[1]) > 10
? 'gap-px'
: 'gap-0.5',
)}
cellProps={{
style: {
borderRadius: `${borderRadius}${borderRadiusUnit}`,
},
}}
duration={Number(duration)}
cellClassName="!size-full"
cellActiveClassName="bg-neutral-800 dark:bg-neutral-200"
cellInactiveClassName="bg-neutral-200 dark:bg-neutral-800"
gridSize={gridSize}
frames={frames}
/>
</div>
<div className="max-w-[150px] sm:max-w-[200px] flex items-center justify-center aspect-square mx-auto">
<div
className={cn(
'grid w-fit p-3 dark:bg-neutral-900 bg-neutral-100 rounded-xl',
gridSize[0] > gridSize[1]
? 'w-[150px] sm:w-[200px]'
: 'h-[150px] sm:h-[200px]',
Math.max(gridSize[0], gridSize[1]) > 10
? 'gap-0.5'
: Math.max(gridSize[0], gridSize[1]) > 10
? 'gap-1'
: 'gap-1.5',
)}
style={{
gridTemplateColumns: `repeat(${gridSize[0]}, minmax(0, 1fr))`,
gridAutoRows: '1fr',
}}
>
{Array.from({ length: gridSize[0]! * gridSize[1]! }).map(
(_, i) => (
<div
key={i}
onMouseDown={() => {
const x = i % gridSize[0]!;
const y = Math.floor(i / gridSize[0]!);
const action: 'add' | 'remove' = activeFrameDots.has(i)
? 'remove'
: 'add';
setDrawAction(action);
setIsDrawing(true);
applyDot(x, y, action);
}}
onMouseEnter={() => {
if (!isDrawing || !drawAction) return;
const x = i % gridSize[0]!;
const y = Math.floor(i / gridSize[0]!);
applyDot(x, y, drawAction);
}}
className={cn(
'rounded-full aspect-square hover:ring hover:ring-neutral-300 dark:hover:ring-neutral-700',
activeFrameDots.has(i)
? 'bg-neutral-800 dark:bg-neutral-200'
: 'bg-neutral-200 dark:bg-neutral-800',
)}
style={{
borderRadius: `${borderRadius}${borderRadiusUnit}`,
}}
/>
),
)}
</div>
</div>
</div>
<TooltipProvider>
<div className="p-4 space-y-4 pt-0">
<div className="h-20 w-full flex flex-row items-center gap-2">
<ScrollArea className="w-full h-full flex-1 bg-neutral-100 dark:bg-neutral-900 rounded-2xl overflow-x-auto">
<div className="w-max p-2 h-full">
<div className="flex h-full flex-row gap-2 items-center">
{frames.map((_, index) => {
const activeDot = new Set<number>(
frames[index]?.map(
([x, y]) => y * gridSize[0]! + x,
) ?? [],
);
return (
<button
key={index}
className={cn(
'h-full cursor-pointer shrink-0 bg-background rounded-lg',
activeFrame === index
? 'ring-2 ring-neutral-300 dark:ring-neutral-700'
: '',
)}
onClick={() => setActiveFrame(index)}
>
<div
className={cn(
'grid p-[5px] h-full',
Math.max(gridSize[0], gridSize[1]) > 10
? 'gap-[0.5px]'
: Math.max(gridSize[0], gridSize[1]) > 10
? 'gap-px'
: 'gap-[1.5px]',
)}
style={{
gridTemplateColumns: `repeat(${gridSize[0]}, minmax(0, 1fr))`,
}}
>
{Array.from({
length: gridSize[0]! * gridSize[1]!,
}).map((_, i) => (
<div
key={i}
className={cn(
'bg-neutral-200 dark:bg-neutral-800 aspect-square w-full',
activeDot.has(i)
? 'bg-neutral-800 dark:bg-neutral-200'
: 'bg-neutral-200 dark:bg-neutral-800',
)}
style={{
borderRadius: `${borderRadius}${borderRadiusUnit}`,
}}
/>
))}
</div>
</button>
);
})}
</div>
</div>
</ScrollArea>
<div className="grid grid-cols-2 gap-2">
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
onClick={() => {
setFrames((prev) => {
const currentIndex = activeFrame;
const currentFrame = prev[currentIndex] ?? [];
const newFrame: Frame = currentFrame.map(
([x, y]) => [x, y],
);
const updatedFrames = [...prev];
updatedFrames.splice(currentIndex + 1, 0, newFrame);
setActiveFrame(currentIndex + 1);
return updatedFrames;
});
}}
>
<PlusIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Add Frame</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
variant="destructive"
disabled={frames.length <= 1}
onClick={() => {
if (frames.length <= 1) return;
setFrames((prev) => {
const newFrames = prev.filter(
(_, i) => i !== activeFrame,
);
setActiveFrame(
newFrames.length - 1 > 0
? newFrames.length - 1
: 0,
);
return newFrames;
});
}}
>
<Trash2Icon />
</Button>
</TooltipTrigger>
<TooltipContent>Remove Frame</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
className="h-8"
variant="neutral"
disabled={frames.length <= 1}
onClick={() => moveCurrentFrame(-1)}
>
<ArrowLeftIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Move Frame Left</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
className="h-8"
variant="neutral"
disabled={frames.length <= 1}
onClick={() => moveCurrentFrame(1)}
>
<ArrowRightIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Move Frame Right</TooltipContent>
</Tooltip>
</div>
</div>
<div className="h-10 flex flex-row gap-4 items-center">
<div className="relative flex-1 h-full bg-neutral-100 dark:bg-neutral-900 rounded-lg">
<div className="absolute inset-y-0 left-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-l-lg flex items-center justify-center">
<Timer className="size-5 text-neutral-400 dark:text-neutral-600" />
</div>
<Input
type="number"
className="size-full px-13 border-none bg-transparent shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={duration}
placeholder="Duration"
step={10}
onChange={(e) => {
const v = e.target.value;
if (v === '') {
setDuration('');
return;
}
const n = Number(v);
if (!Number.isNaN(n) && n >= 0) {
setDuration(v);
}
}}
/>
<div className="absolute inset-y-0 right-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-r-lg flex items-center justify-center">
<span className="text-neutral-400 dark:text-neutral-600 text-sm">
ms
</span>
</div>
</div>
<div className="relative flex-1 h-full bg-neutral-100 dark:bg-neutral-900 rounded-lg">
<div className="absolute inset-y-0 left-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-l-lg flex items-center justify-center">
<SquareRoundCorner className="size-5 text-neutral-400 dark:text-neutral-600" />
</div>
<Input
type="number"
className="size-full px-13 border-none bg-transparent shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={borderRadius}
placeholder="Border Radius"
onChange={(e) => {
const v = e.target.value;
if (v === '') {
setBorderRadius('');
return;
}
const n = Number(v);
if (!Number.isNaN(n) && n >= 0) {
setBorderRadius(v);
}
}}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="absolute inset-y-0 right-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-r-lg flex items-center justify-center">
<button className="text-neutral-400 dark:text-neutral-600 text-sm">
{borderRadiusUnit}
</button>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
{BORDER_RADIUS_UNITS.map((unit) => (
<DropdownMenuItem
key={unit}
onClick={() => setBorderRadiusUnit(unit)}
>
{unit}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<div className="flex p-4 flex-row gap-2 border-t">
<Input
required
placeholder="Animation Name"
className="border-none bg-neutral-100 dark:bg-neutral-900 shadow-none"
value={animationName}
onChange={(e) => setAnimationName(e.target.value)}
/>
<Tooltip>
<TooltipTrigger>
<Button
variant="neutral"
size="icon"
onClick={() => setFrames([[]])}
>
<RotateCcwIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Reset Animation</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variant="neutral"
size="icon"
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(frames));
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
}}
>
{isCopied ? <CheckIcon /> : <CopyIcon />}
</Button>
</TooltipTrigger>
<TooltipContent>
{isCopied ? 'Copied' : 'Copy Animation'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
disabled={!animationName.trim()}
onClick={() => {
if (!animationName.trim())
return alert('Please enter a name');
const nameAlreadyExists = selectedAnimation
? animationName !== selectedAnimation &&
animations[animationName]
: Boolean(animations[animationName]);
if (nameAlreadyExists) {
return alert('Animation with this name already exists');
}
const newAnimations = { ...animations };
if (selectedAnimation) {
if (selectedAnimation !== animationName) {
delete newAnimations[selectedAnimation];
}
}
newAnimations[animationName] = {
gridSize,
frames,
duration,
borderRadius,
borderRadiusUnit,
};
setAnimations(newAnimations);
setSelectedAnimation(animationName);
localStorage.setItem(
'animations',
JSON.stringify(newAnimations),
);
setIsSaved(true);
setTimeout(() => setIsSaved(false), 2000);
}}
>
{isSaved ? <CheckIcon /> : <SaveIcon />}
</Button>
</TooltipTrigger>
<TooltipContent>Save Animation</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,29 @@
export const Footer = () => {
return (
<div className="h-[55px] border-t">
<div className="max-w-7xl mx-auto h-full">
<div className="size-full px-4 md:px-6 flex items-center justify-start prose prose-sm text-sm text-muted-foreground">
<p className="text-start truncate">
Built by{' '}
<a
href="https://x.com/imskyleen"
rel="noopener noreferrer"
target="_blank"
>
Skyleen
</a>
. The source code is available on{' '}
<a
href="https://github.com/animate-ui/animate-ui"
rel="noopener noreferrer"
target="_blank"
>
GitHub
</a>
.
</p>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,124 @@
'use client';
import { motion } from 'motion/react';
import { Logo } from '@/components/logo';
import GithubIcon from '@workspace/ui/components/icons/github-icon';
import XIcon from '@workspace/ui/components/icons/x-icon';
import { useIsMobile } from '@workspace/ui/hooks/use-mobile';
import { useEffect, useState } from 'react';
import { ThemeSwitcher } from './theme-switcher';
const LOGO_WRAPPER_VARIANTS = {
center: {
top: 0,
left: 0,
right: 0,
bottom: 0,
height: '100%',
},
topLeft: {
top: 0,
left: 0,
right: 0,
bottom: 'auto',
height: 'auto',
},
};
const logoVariants = (isScroll: boolean, isMobile: boolean) => ({
center: {
top: '50%',
left: '50%',
x: '-50%',
y: '-50%',
scale: 1,
},
topLeft: {
top: isScroll ? (isMobile ? 4 : 0) : 20,
left: isScroll ? (isMobile ? -36 : -61) : isMobile ? -36 : -43,
x: 0,
y: 0,
scale: isScroll ? (isMobile ? 0.6 : 0.5) : 0.6,
},
});
export const Header = ({ transition }: { transition: boolean }) => {
const isMobile = useIsMobile();
const [isScroll, setIsScroll] = useState(false);
useEffect(() => {
const handleScroll = () => setIsScroll(window.scrollY > 10);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<motion.div
variants={LOGO_WRAPPER_VARIANTS}
initial="center"
animate={transition ? 'topLeft' : 'center'}
transition={{ type: 'spring', stiffness: 200, damping: 30 }}
className="fixed z-40 flex items-center justify-center"
>
<motion.div className="absolute inset-x-0 top-0 h-14 z-100 w-full bg-background/70 backdrop-blur-md" />
<div className="relative max-w-7xl size-full">
<motion.div
className="absolute z-110"
variants={logoVariants(isScroll, isMobile)}
initial="center"
animate={transition ? 'topLeft' : 'center'}
transition={{ type: 'spring', stiffness: 200, damping: 30 }}
>
<Logo size={isMobile ? 'lg' : 'xl'} draw betaTag />
</motion.div>
<motion.div
initial={{
top: isScroll ? (isMobile ? 12 : 7.5) : 28,
right: -43,
opacity: 0,
}}
animate={
transition
? {
top: isScroll ? (isMobile ? 12 : 7.5) : 28,
right: 20,
opacity: 1,
}
: {
top: isScroll ? (isMobile ? 12 : 7.5) : 28,
right: -43,
opacity: 0,
}
}
transition={{ type: 'spring', stiffness: 200, damping: 30 }}
className="absolute z-110 flex items-center gap-x-4"
>
<div className="hidden xs:flex items-center gap-x-1">
<a
href="https://github.com/animate-ui/animate-ui"
rel="noreferrer noopener"
target="_blank"
className="inline-flex sm:mt-1 items-center justify-center rounded-md text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 hover:bg-fd-accent hover:text-fd-accent-foreground p-1.5 [&_svg]:size-5 text-fd-muted-foreground sm:[&_svg]:size-5.5"
data-active="false"
>
<GithubIcon />
</a>
<a
href="https://x.com/animate_ui"
rel="noreferrer noopener"
target="_blank"
className="inline-flex sm:mt-1 items-center justify-center rounded-md text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 hover:bg-fd-accent hover:text-fd-accent-foreground p-1.5 [&_svg]:size-5 text-fd-muted-foreground sm:[&_svg]:size-5.5"
data-active="false"
>
<XIcon />
</a>
</div>
<ThemeSwitcher className="mt-1 xs:mt-0 sm:mt-1" />
</motion.div>
</div>
</motion.div>
);
};

View File

@ -0,0 +1,200 @@
'use client';
import { ArrowRightIcon, Moon, Sun } from 'lucide-react';
import Link from 'next/link';
import { Button } from '@workspace/ui/components/ui/button';
import { HighlightText } from '@/registry/text/highlight';
import { motion } from 'motion/react';
import { Tabs, TabsList, TabsTrigger } from '@/registry/components/tabs';
import { Switch } from '@/registry/radix/switch';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '@/registry/radix/accordion';
import { RadixProgressDemo } from '@/registry/demo/radix/progress';
import { CodeTabs } from '@/registry/components/code-tabs';
import { Checkbox } from '@/registry/radix/checkbox';
import { GitHubStarsButton } from '@/registry/buttons/github-stars';
import ReactIcon from '@workspace/ui/components/icons/react-icon';
import TSIcon from '@workspace/ui/components/icons/ts-icon';
import TailwindIcon from '@workspace/ui/components/icons/tailwind-icon';
import MotionIcon from '@workspace/ui/components/icons/motion-icon';
import ShadcnIcon from '@workspace/ui/components/icons/shadcn-icon';
const COMMANDS = {
npm: `npx shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
pnpm: `pnpm dlx shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
yarn: `npx shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
bun: `bun x --bun shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
};
const ACCORDION_ITEMS = [
{
value: 'item-1',
title: 'What is Animate UI?',
content:
'Animate UI is an open-source distribution of React components built with TypeScript, Tailwind CSS, and Motion.',
},
{
value: 'item-2',
title: 'How is it different from other libraries?',
content:
'Instead of installing via NPM, you copy and paste the components directly. This gives you full control to modify or customize them as needed.',
},
{
value: 'item-3',
title: 'Is Animate UI free to use?',
content:
'Absolutely! Animate UI is fully open-source. You can use, modify, and adapt it to fit your needs.',
},
];
export const Hero = () => {
const { resolvedTheme: theme, setTheme } = useTheme();
const [checkboxChecked, setCheckboxChecked] = useState<
boolean | 'indeterminate'
>(false);
useEffect(() => {
setTimeout(() => setCheckboxChecked(true), 1000);
}, []);
return (
<div className="relative mx-auto max-w-7xl px-6 pt-8 w-full flex flex-col gap-10">
<div className="lg:max-w-[50%] max-w-[700px] space-y-6 md:mt-20 mt-8">
<h1 className="text-3xl md:text-4xl lg:text-[43px] sm:text-start text-center font-semibold text-neutral-800 dark:text-white !leading-relaxed lg:!leading-snug">
Elevate your UI with fluid,{' '}
<HighlightText
transition={{ duration: 2, delay: 0.5, ease: 'easeInOut' }}
inView
text="animated components"
className="from-blue-100 to-blue-100 dark:from-blue-500 dark:to-blue-500"
/>
</h1>
<p className="text-base sm:text-start text-center text-neutral-500 dark:text-neutral-400 max-w-2xl">
A fully animated, open-source component distribution built with{' '}
<strong>
React, TypeScript, Tailwind CSS, Motion and Shadcn CLI
</strong>
. Browse a list of components you can install, modify, and use in your
projects.
</p>
<div className="flex sm:flex-row flex-col sm:gap-5 gap-3 my-8">
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
size="lg"
className="w-full rounded-full pr-5 bg-blue-500 text-white hover:bg-blue-500/90"
asChild
>
<Link href="/docs">
Get Started <ArrowRightIcon className="!size-5" />
</Link>
</Button>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
size="lg"
className="w-full rounded-full"
variant="neutral"
asChild
>
<Link href="/docs/components">Browse Components</Link>
</Button>
</motion.div>
</div>
<div className="flex items-center gap-4 justify-center sm:justify-start">
<ReactIcon className="size-8" />
<TSIcon className="size-8" />
<TailwindIcon className="size-8" />
<MotionIcon className="size-12" />
<ShadcnIcon className="size-8" />
</div>
</div>
<div className="hidden lg:block">
<div className="absolute top-10 right-8">
<Tabs>
<TabsList className="w-[250px]">
<TabsTrigger className="w-full" value="code">
Code
</TabsTrigger>
<TabsTrigger className="w-full" value="issues">
Issues
</TabsTrigger>
<TabsTrigger className="w-full" value="docs">
Docs
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="absolute top-28 right-4">
<CodeTabs
defaultValue="pnpm"
className="max-w-[520px]"
codes={COMMANDS}
/>
</div>
<div className="absolute top-62 right-37">
<RadixProgressDemo />
</div>
<div className="absolute top-80 right-115">
<Checkbox
className="scale-125"
checked={checkboxChecked}
onCheckedChange={(checked) => setCheckboxChecked(checked)}
/>
</div>
<div className="absolute top-58 right-14">
<Switch
className="scale-125"
leftIcon={<Sun />}
rightIcon={<Moon />}
checked={theme === 'dark'}
onCheckedChange={(checked) => setTheme(checked ? 'dark' : 'light')}
/>
</div>
<div className="absolute top-4 right-85">
<GitHubStarsButton repo="animate-ui" username="animate-ui" />
</div>
<div className="absolute top-75 right-4">
<Accordion
type="single"
defaultValue="item-1"
collapsible
className="w-[400px] rounded-xl border overflow-hidden"
>
{ACCORDION_ITEMS.map((item) => (
<AccordionItem
key={item.value}
value={item.value}
className="last:border-b-0"
>
<AccordionTrigger className="text-sm px-4 py-3 bg-muted">
{item.title}
</AccordionTrigger>
<AccordionContent className="p-4 border-t text-muted-foreground">
{item.content}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,85 @@
'use client';
import { cn } from '@workspace/ui/lib/utils';
import { motion, type SVGMotionProps } from 'motion/react';
const pathVariants = {
hidden: {
pathLength: 0,
fillOpacity: 0,
},
visible: {
pathLength: 1,
fillOpacity: 1,
transition: {
duration: 2,
ease: 'easeInOut',
},
},
} as const;
const sizes = {
sm: {
svg: 'h-6',
betaTag: 'bottom-[2px] left-[calc(100%+6px)] px-1.5 py-0.5 text-[9px]',
},
lg: {
svg: 'h-12',
betaTag: 'bottom-[4px] left-[calc(100%+10px)] px-2 py-0.5 text-base',
},
xl: {
svg: 'h-14',
betaTag: 'bottom-[7px] left-[calc(100%+15px)] px-2.5 py-1 text-base',
},
};
export const Logo = ({
betaTag = false,
draw = false,
size = 'sm',
className,
containerClassName,
...props
}: {
containerClassName?: string;
betaTag?: boolean;
draw?: boolean;
size?: keyof typeof sizes;
} & SVGMotionProps<SVGSVGElement>) => {
return (
<div className={cn('relative', containerClassName)}>
<motion.svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 4899.14 783.54"
className={cn(sizes[size].svg, className)}
{...props}
>
<motion.path
variants={draw ? pathVariants : {}}
initial={draw ? 'hidden' : false}
animate={draw ? 'visible' : false}
stroke="currentColor"
strokeWidth={4}
className="fill-neutral-900 dark:fill-neutral-100"
d="M612.79 783.54H227.43c-41.6 0-82.15-11.19-117.26-32.37-32.96-19.88-60.58-48.18-79.88-81.85C10.99 635.66.52 597.52.02 559.03c-.54-41 10.29-81.64 31.32-117.54l192.67-329.07c20.81-35.55 50.62-64.69 86.19-84.28C343.64 9.73 381.64 0 420.11 0s76.47 9.73 109.9 28.14c35.57 19.59 65.38 48.73 86.19 84.28l192.68 329.07c21.02 35.9 31.85 76.55 31.31 117.54-.5 38.49-10.97 76.62-30.27 110.29s-46.92 61.97-79.88 81.85c-35.11 21.18-75.65 32.37-117.26 32.37ZM396.6 213.47 203.92 542.54c-6.59 11.26-3.71 21.06-.13 27.31s10.59 13.69 23.63 13.69h385.36c13.04 0 20.05-7.45 23.63-13.69 3.58-6.25 6.46-16.05-.12-27.31L443.61 213.47c-6.52-11.14-16.38-13.47-23.5-13.47s-16.98 2.34-23.5 13.47ZM1574.55 673.25h112.71l-218.43-546.46h-87.06l-220.76 546.46h111.94l38.89-101.83h224.17zM1345.68 482.8l78.75-206.19 78.04 206.19zM2067.37 364.26c-12.44-22.02-29.42-39.9-50.91-53.64-21.51-13.73-46.25-20.6-74.24-20.6s-54.02 6.22-76.57 18.66c-12.2 6.73-22.94 14.88-32.26 24.42v-35.3h-101.83v375.45h101.83V457.93c0-14.5 3.23-27.59 9.72-39.25q9.705-17.49 26.82-26.82c11.39-6.22 24.35-9.33 38.87-9.33 21.77 0 39.64 7 53.64 20.99s20.99 32.14 20.99 54.41v215.32h102.61V434.61c0-24.88-6.22-48.32-18.66-70.35ZM2204.18 237.17c-16.59 0-30.32-5.56-41.2-16.71-10.88-11.14-16.32-25-16.32-41.59s5.44-29.79 16.32-41.2c10.88-11.39 24.61-17.1 41.2-17.1s30.96 5.71 41.59 17.1c10.62 11.41 15.93 25.14 15.93 41.2s-5.32 30.45-15.93 41.59c-10.63 11.15-24.49 16.71-41.59 16.71m-52.08 436.08V297.8h103.38v375.45zM2902.6 357.65c-13.21-21.77-30.97-38.48-53.25-50.14-22.29-11.66-47.42-17.49-75.4-17.49s-53.38 6.1-76.18 18.27c-18.12 9.68-33.52 22.38-46.24 38.11-11.59-16.2-26.23-29.17-43.93-38.88q-31.875-17.49-70.74-17.49c-27.47 0-52.08 5.83-73.85 17.49-11.67 6.25-22.03 13.9-31.09 22.91V297.8h-101.83v375.45h101.83V452.49c0-15.02 3.23-27.72 9.72-38.09 6.47-10.36 15.03-18.27 25.65-23.71s22.66-8.16 36.15-8.16c20.21 0 37.04 6.1 50.53 18.27 13.47 12.18 20.21 29.15 20.21 50.92v221.54h102.61V452.5c0-15.02 3.23-27.72 9.72-38.09 6.47-10.36 15.02-18.27 25.65-23.71 10.62-5.44 22.93-8.16 36.92-8.16 19.69 0 36.27 6.1 49.75 18.27 13.47 12.18 20.21 29.15 20.21 50.92v221.54h103.38V436.18c0-30.57-6.61-56.75-19.82-78.51ZM3253.56 297.8v35.43c-10.39-10.75-22.56-19.72-36.54-26.88-21.25-10.88-45.08-16.32-71.51-16.32-34.73 0-65.69 8.55-92.89 25.65-27.21 17.1-48.58 40.42-64.13 69.96s-23.32 62.96-23.32 100.28 7.77 69.96 23.32 99.5 36.92 52.86 64.13 69.96 57.91 25.65 92.11 25.65c26.94 0 51.17-5.44 72.68-16.32 13.89-7.03 25.93-15.85 36.15-26.45v35h102.61V297.81h-102.61Zm-20.21 260.4c-17.62 19.18-40.69 28.76-69.18 28.76-18.15 0-34.47-4.4-48.97-13.21-14.51-8.81-25.79-20.72-33.81-35.76-8.04-15.02-12.05-32.65-12.05-52.86s4.01-37.04 12.05-52.08c8.03-15.02 19.17-26.94 33.43-35.76 14.25-8.81 30.71-13.21 49.36-13.21s35.76 4.28 49.75 12.83 25.13 20.6 33.43 36.15c8.28 15.55 12.44 33.17 12.44 52.86 0 29.03-8.82 53.13-26.43 72.29ZM3674.09 297.8h-87.84V141.55h-101.83V297.8h-87.84v89.39h87.84v286.06h101.83V387.19h87.84zM4049.53 380.2c-15.55-28.49-37.31-50.78-65.3-66.85-27.98-16.06-59.6-24.1-94.83-24.1-37.31 0-71 8.55-101.05 25.65q-45.09 25.65-71.13 69.96c-17.37 29.54-26.04 62.96-26.04 100.28s8.81 71.51 26.43 101.05c17.61 29.54 41.84 52.86 72.68 69.96q46.245 25.65 105.33 25.65c30.57 0 58.81-5.44 84.73-16.32 25.91-10.88 48.45-27.21 67.63-48.97l-60.63-60.63c-11.4 13.48-24.87 23.32-40.42 29.54s-32.92 9.33-52.08 9.33c-21.25 0-39.91-4.4-55.97-13.21-16.07-8.81-28.37-21.63-36.92-38.48-3.4-6.69-6.11-13.85-8.15-21.48l274.39-.67c2.06-8.28 3.36-15.93 3.89-22.93.51-7 .78-13.86.78-20.6 0-36.27-7.77-68.66-23.32-97.17Zm-213.76 7.77c15.02-8.81 32.65-13.21 52.86-13.21 19.17 0 35.24 3.89 48.2 11.66 12.95 7.77 23.05 19.18 30.32 34.2 3.49 7.23 6.19 15.32 8.12 24.23l-181.45.52c1.88-6.99 4.34-13.56 7.37-19.7 8.03-16.32 19.55-28.88 34.59-37.7ZM4484.04 681.8c-43.02 0-81.36-9.58-115.05-28.76-33.69-19.17-60.24-45.34-79.68-78.51-19.43-33.16-29.15-70.74-29.15-112.71V126.78h106.49v338.14c0 24.36 5.17 45.35 15.55 62.96 10.36 17.62 24.35 31.23 41.98 40.81 17.61 9.59 37.57 14.38 59.85 14.38s41.84-4.79 58.69-14.38c16.83-9.58 30.32-23.19 40.42-40.81 10.1-17.61 15.16-38.34 15.16-62.19V126.78h106.5v335.81c0 41.98-9.46 79.42-28.37 112.33q-28.38 49.38-78.12 78.12c-33.17 19.18-71.26 28.76-114.27 28.76M4793.42 673.25V126.78h105.72v546.46h-105.72Z"
></motion.path>
</motion.svg>
{betaTag && (
<motion.div
className={cn(
sizes[size].betaTag,
'absolute bg-neutral-200 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 rounded-full',
)}
initial={draw ? { opacity: 0 } : undefined}
animate={draw ? { opacity: 1 } : undefined}
transition={{ duration: 4, ease: 'easeInOut' }}
>
Beta
</motion.div>
)}
<span className="sr-only">Animate UI</span>
</div>
);
};

View File

@ -0,0 +1,39 @@
import { cn } from '@workspace/ui/lib/utils';
import { Button } from '@workspace/ui/components/ui/button';
import Link from 'next/link';
import { motion } from 'motion/react';
export const SectionWrapper = ({
title,
subtitle,
description,
color = 'text-blue-500',
backgroundColor = 'bg-blue-500/10 hover:bg-blue-500/20',
children,
}: {
title: React.ReactNode;
subtitle: React.ReactNode;
description: React.ReactNode;
color?: string;
backgroundColor?: string;
children: React.ReactNode;
}) => {
return (
<section className="mt-30">
<p className={cn('text-base font-medium mb-4', color)}>{subtitle}</p>
<h2 className="text-3xl md:text-4xl font-semibold">{title}</h2>
<p className="mt-4 text-muted-foreground max-w-3xl">{description}</p>
<motion.div className="my-8 w-fit" whileTap={{ scale: 0.95 }}>
<Button
asChild
className={cn('rounded-full transition-colors', backgroundColor)}
>
<Link href="/docs">
<span className={color}>Get Started</span>
</Link>
</Button>
</motion.div>
{children}
</section>
);
};

View File

@ -0,0 +1,28 @@
'use client';
import { Switch } from '@/registry/radix/switch';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export const ThemeSwitcher = ({ className }: { className?: string }) => {
const { resolvedTheme: theme, setTheme } = useTheme();
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return (
isClient && (
<Switch
className={className}
leftIcon={<Sun />}
rightIcon={<Moon />}
checked={theme === 'dark'}
onCheckedChange={(checked) => setTheme(checked ? 'dark' : 'light')}
/>
)
);
};

View File

@ -0,0 +1,60 @@
---
title: Bubble Background
description: An interactive background featuring smoothly animated gradient bubbles, creating a playful, dynamic, and visually engaging backdrop.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="bubble-background-demo" />
## Installation
<ComponentInstallation name="bubble-background" />
## Usage
```tsx
<BubbleBackground />
```
## Props
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: false,
},
interactive: {
description: 'Whether the background is interactive',
type: 'boolean',
default: 'false',
required: false,
},
transition: {
description: 'The transition of the interactive bubble',
type: 'SpringOptions',
required: false,
default: '{ stiffness: 100, damping: 20 }',
},
colors: {
description: 'The colors of the background',
type: `object`,
default: `{
first: '18,113,255',
second: '221,74,255',
third: '0,220,255',
fourth: '200,50,50',
fifth: '180,180,50',
sixth: '140,100,255',
}`,
required: false,
},
}}
/>
## Credits
- Credits to [GlitchWorker](https://codepen.io/glitchworker/pen/jENZGOV) for the inspiration

View File

@ -0,0 +1,97 @@
---
title: Fireworks Background
description: A background component that displays a fireworks animation.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="fireworks-background-demo" />
_Click on the background to make it rain fireworks._
## Installation
<ComponentInstallation name="fireworks-background" />
## Usage
```tsx
<FireworksBackground />
```
## Examples
### With a high population
<ComponentPreview name="fireworks-background-population-demo" />
### With a high size and speed
<ComponentPreview name="fireworks-background-size-speed-demo" />
### Circle effect with a fixed size and speed
<ComponentPreview name="fireworks-background-fix-size-speed-demo" />
## Props
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: false,
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
canvasProps: {
description: 'The props for the canvas element',
type: 'React.HTMLAttributes<HTMLCanvasElement>',
required: false,
},
color: {
description: 'The color of the fireworks',
type: 'string | string[]',
required: false,
default: 'randColor()',
},
population: {
description: 'The number of fireworks to display',
type: 'number',
required: false,
default: 1,
},
fireworkSpeed: {
description: 'The speed of the fireworks',
type: '{ min: number; max: number } | number',
required: false,
default: '{ min: 4, max: 8 }',
},
fireworkSize: {
description: 'The size of the fireworks',
type: '{ min: number; max: number } | number',
required: false,
default: '{ min: 2, max: 5 }',
},
particleSpeed: {
description: 'The speed of the particles',
type: '{ min: number; max: number } | number',
required: false,
default: '{ min: 2, max: 7 }',
},
particleSize: {
description: 'The size of the particles',
type: '{ min: number; max: number } | number',
required: false,
default: '{ min: 1, max: 5 }',
},
}}
/>
## Credits
- Credits to [Matt Cannon](https://codepen.io/matt-cannon/pen/YPKGBGm) for the inspiration

View File

@ -0,0 +1,42 @@
---
title: Gradient Background
description: A background component featuring a subtle yet engaging animated gradient effect, smoothly transitioning colors to enhance visual depth.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="gradient-background-demo" />
## Installation
<ComponentInstallation name="gradient-background" />
## Usage
```tsx
<GradientBackground />
```
## Props
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: false,
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
transition: {
description: 'The transition of the gradient',
type: 'Transition',
required: false,
default: "{ duration: 15, ease: 'easeInOut', repeat: Infinity }",
},
}}
/>

View File

@ -0,0 +1,57 @@
---
title: Hexagon Background
description: A background component featuring an interactive hexagon grid.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="hexagon-background-demo" />
## Installation
<ComponentInstallation name="hexagon-background" />
## Usage
```tsx
<HexagonBackground />
```
## Props
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: false,
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
hexagonProps: {
description: 'The props of the hexagon',
type: 'React.HTMLAttributes<HTMLDivElement>',
required: false,
},
hexagonSize: {
description: 'The size of the hexagon',
type: 'number',
required: false,
default: '75',
},
hexagonMargin: {
description: 'The margin of the hexagon',
type: 'number',
required: false,
default: '3',
},
}}
/>
## Credits
- Credits to [Denis Klak](https://codepen.io/d3nis031/pen/QWyeNYx) for the inspiration

View File

@ -0,0 +1,64 @@
---
title: Hole Background
description: A background with a hole animation effect.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="hole-background-demo" />
## Installation
<ComponentInstallation name="hole-background" />
## Usage
```tsx
<HoleBackground />
```
## Props
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: false,
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
strokeColor: {
description: 'The color of the stroke',
type: 'string',
required: false,
default: '#737373',
},
numberOfLines: {
description: 'The number of lines',
type: 'number',
required: false,
default: '50',
},
numberOfDiscs: {
description: 'The number of discs',
type: 'number',
required: false,
default: '50',
},
particleRGBColor: {
description: 'The RGB color of the particles',
type: 'number[]',
required: false,
default: '[255, 255, 255]',
},
}}
/>
## Credits
- Credits to [Antoine Wodniack](https://codepen.io/wodniack/pen/VYwGEGg) for the inspiration

View File

@ -0,0 +1,104 @@
---
title: Stars Background
description: A dark, interactive background featuring animated dots of varying sizes and speeds, simulating a dynamic and immersive starry space effect.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="stars-background-demo" />
## Installation
<ComponentInstallation name="stars-background" />
## Usage
```tsx
<StarsBackground />
```
## Props
### StarsBackground
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: false,
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
factor: {
description: 'The factor of the component',
type: 'number',
default: '0.05',
required: false,
},
speed: {
description: 'The speed of the component',
type: 'number',
default: '50',
required: false,
},
transition: {
description: 'The transition of the interactive movement',
type: 'SpringOptions',
required: false,
default: '{ stiffness: 50, damping: 20 }',
},
starColor: {
description: 'The color of the stars',
type: 'string',
default: '#fff',
required: false,
},
pointerEvents: {
description:
'Controls whether pointer events are caught by the component or pass through to elements behind it',
type: 'boolean',
default: 'true',
required: false,
},
}}
/>
### StarLayer
<TypeTable
type={{
count: {
description: 'The number of stars',
type: 'number',
default: '1000',
required: false,
},
size: {
description: 'The size of the stars',
type: 'number',
default: '1',
required: false,
},
transition: {
description: 'The transition of the stars',
type: 'Transition',
required: false,
default: '{ repeat: Infinity, duration: 50, ease: "linear" }',
},
starColor: {
description: 'The color of the stars',
type: 'string',
default: '#fff',
required: false,
},
}}
/>
## Credits
- Credits to [umangladani](https://codepen.io/umangladani/pen/wvgwgjK) for the inspiration

View File

@ -0,0 +1,84 @@
---
title: Accordion
description: A set of collapsible panels with headings.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-accordion-demo" />
## Installation
<ComponentInstallation name="base-accordion" />
## Usage
```tsx
<Accordion>
<AccordionItem>
<AccordionTrigger>Accordion Item 1</AccordionTrigger>
<AccordionPanel>Accordion Panel 1</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionTrigger>Accordion Item 2</AccordionTrigger>
<AccordionPanel>Accordion Panel 2</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionTrigger>Accordion Item 3</AccordionTrigger>
<AccordionPanel>Accordion Panel 3</AccordionPanel>
</AccordionItem>
</Accordion>
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/accordion"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/accordion#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### AccordionTrigger
<TypeTable
type={{
transition: {
description: 'The transition of the accordion chevron',
type: 'Transition',
required: false,
default: "{ type: 'spring', stiffness: 150, damping: 17 }",
},
chevron: {
description: 'Whether to show the accordion chevron',
type: 'boolean',
required: false,
default: 'true',
},
}}
/>
#### AccordionPanel
<TypeTable
type={{
transition: {
description: 'The transition of the accordion panel',
type: 'Transition',
required: false,
default: "{ type: 'spring', stiffness: 150, damping: 22 }",
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/accordion) for the accordion component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/accordion) for the accordion style.

View File

@ -0,0 +1,51 @@
---
title: Checkbox
description: An easily stylable checkbox component.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-checkbox-demo" />
## Installation
<ComponentInstallation name="base-checkbox" />
## Usage
```tsx
<Checkbox />
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/checkbox"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/checkbox#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### Checkbox
<TypeTable
type={{
motionProps: {
description: 'The props to pass to the motion component.',
type: "HTMLMotionProps<'button'>",
required: false,
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/checkbox) for the checkbox component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/checkbox) for the checkbox style.

View File

@ -0,0 +1,70 @@
---
title: Popover
description: An accessible popup anchored to a button.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-popover-demo" />
## Installation
<ComponentInstallation name="base-popover" />
## Usage
```tsx
<Popover>
<PopoverTrigger>Open Popover</PopoverTrigger>
<PopoverContent>Popover Content</PopoverContent>
</Popover>
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/popover"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/popover#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### PopoverContent
<TypeTable
type={{
positionerClassName: {
description: 'The class name to apply to the positioner.',
type: 'string',
required: false,
},
motionProps: {
description: 'The props to pass to the motion component.',
type: "HTMLMotionProps<'div'>",
required: false,
},
popupProps: {
description: 'The props to pass to the popup component.',
type: 'typeof PopoverPrimitive.Popup',
required: false,
},
transition: {
description: 'The transition to use for the popover.',
type: 'Transition',
required: false,
default: '{ type: "spring", stiffness: 300, damping: 25 }',
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/popover) for the popover component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/popover) for the popover style.

View File

@ -0,0 +1,70 @@
---
title: Preview Card
description: A popup that appears when a link is hovered, showing a preview for sighted users.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-preview-card-demo" />
## Installation
<ComponentInstallation name="base-preview-card" />
## Usage
```tsx
<PreviewCard>
<PreviewCardTrigger>Hover me</PreviewCardTrigger>
<PreviewCardContent>Preview card content</PreviewCardContent>
</PreviewCard>
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/preview-card"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/preview-card#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### PreviewCardContent
<TypeTable
type={{
positionerClassName: {
description: 'The class name to apply to the positioner.',
type: 'string',
required: false,
},
motionProps: {
description: 'The props to pass to the motion component.',
type: "HTMLMotionProps<'div'>",
required: false,
},
popupProps: {
description: 'The props to pass to the popup component.',
type: 'typeof PopoverPrimitive.Popup',
required: false,
},
transition: {
description: 'The transition to use for the popover.',
type: 'Transition',
required: false,
default: '{ type: "spring", stiffness: 300, damping: 25 }',
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/preview-card) for the preview card component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/preview-card) for the preview card style.

View File

@ -0,0 +1,68 @@
---
title: Progress
description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-progress-demo" />
## Installation
<ComponentInstallation name="base-progress" />
## Usage
```tsx
<Progress value={50}>
<ProgressLabel />
<ProgressValue />
<ProgressTrack />
</Progress>
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/progress"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/progress#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### ProgressTrack
<TypeTable
type={{
transition: {
description: 'The transition to use for the progress track.',
type: 'Transition',
required: false,
default: "{ type: 'spring', stiffness: 100, damping: 30 }",
},
}}
/>
#### ProgressLabel
<TypeTable
type={{
countingNumberProps: {
description: 'The props to pass to the counting number.',
type: 'CountingNumberProps',
required: false,
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/progress) for the progress component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/progress) for the progress style.

View File

@ -0,0 +1,66 @@
---
title: Switch
description: A control that indicates whether a setting is on or off.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-switch-demo" />
## Installation
<ComponentInstallation name="base-switch" />
## Usage
```tsx
<Switch />
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/switch"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/switch#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### Switch
<TypeTable
type={{
motionProps: {
description: 'The props to pass to the motion component.',
type: "HTMLMotionProps<'button'>",
required: false,
},
leftIcon: {
description: 'The icon to display on the left of the switch',
type: 'React.ReactNode',
required: false,
},
rightIcon: {
description: 'The icon to display on the right of the switch',
type: 'React.ReactNode',
required: false,
},
thumbIcon: {
description: 'The icon to display on the thumb of the switch',
type: 'React.ReactNode',
required: false,
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/switch) for the switch component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/switch) for the switch style.

View File

@ -0,0 +1,108 @@
---
title: Toggle Group
description: Provides a shared state to a series of toggle buttons.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-toggle-group-demo" />
## Installation
<ComponentInstallation name="base-toggle-group" />
## Usage
```tsx
<ToggleGroup>
<ToggleGroupItem>A</ToggleGroupItem>
<ToggleGroupItem>B</ToggleGroupItem>
<ToggleGroupItem>C</ToggleGroupItem>
</ToggleGroup>
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/toggle-group"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/toggle-group#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### ToggleGroup
<TypeTable
type={{
transition: {
description: 'The transition of the toggle group',
type: 'Transition',
required: false,
default: "{ type: 'spring', bounce: 0, stiffness: 200, damping: 25 }",
},
activeClassName: {
description: 'The class name of the active toggle group item',
type: 'string',
required: false,
},
variant: {
description: 'The variant of the toggle group',
type: 'default' | 'outline',
required: false,
default: 'default',
},
size: {
description: 'The size of the toggle group',
type: 'default' | 'sm' | 'lg',
required: false,
default: 'default',
},
}}
/>
#### ToggleGroupItem
<TypeTable
type={{
children: {
description: 'The children of the toggle group item',
type: 'React.ReactNode',
required: true,
},
buttonProps: {
description: 'The props of the toggle group item button',
type: 'HTMLMotionProps<"button">',
required: false,
},
spanProps: {
description: 'The props of the toggle group item span',
type: 'React.HTMLAttributes<HTMLSpanElement>',
required: false,
},
variant: {
description: 'The variant of the toggle group item',
type: 'default' | 'outline',
required: false,
default: 'default',
},
size: {
description: 'The size of the toggle group item',
type: 'default' | 'sm' | 'lg',
required: false,
default: 'default',
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/accordion) for the accordion component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/accordion) for the accordion style.

View File

@ -0,0 +1,79 @@
---
title: Tooltip
description: A popup that appears when an element is hovered or focused, showing a hint for sighted users.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="base-tooltip-demo" />
## Installation
<ComponentInstallation name="base-tooltip" />
## Usage
```tsx
<TooltipProvider>
<Tooltip>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>Tooltip content</TooltipContent>
</Tooltip>
</TooltipProvider>
```
## Props
<div className="flex flex-row gap-x-3">
<ExternalLink
href="https://base-ui.com/react/components/tooltip"
text="Docs"
/>
<ExternalLink
href="https://base-ui.com/react/components/tooltip#api-reference"
text="API Reference"
/>
</div>
### Animate UI Props
#### TooltipContent
<TypeTable
type={{
positionerClassName: {
description: 'The class name to apply to the positioner.',
type: 'string',
required: false,
},
motionProps: {
description: 'The props to pass to the motion component.',
type: "HTMLMotionProps<'div'>",
required: false,
},
popupProps: {
description: 'The props to pass to the popup component.',
type: 'typeof PopoverPrimitive.Popup',
required: false,
},
transition: {
description: 'The transition to use for the popover.',
type: 'Transition',
required: false,
default: '{ type: "spring", stiffness: 300, damping: 25 }',
},
arrow: {
description:
'Whether to show the arrow. (only available in shadcn-new-york or default style)',
type: 'boolean',
required: false,
default: 'true',
},
}}
/>
## Credits
- We use [Base UI](https://base-ui.com/react/components/tooltip) for the tooltip component.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/tooltip) for the tooltip style.

View File

@ -0,0 +1,58 @@
---
title: Copy Button
description: A button with a copy to clipboard animation.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="copy-button-demo" />
## Installation
<ComponentInstallation name="copy-button" />
## Usage
```tsx
<CopyButton content="Hello world!" />
```
## Props
<TypeTable
type={{
content: {
description: 'The content to copy to the clipboard.',
type: 'string',
required: false,
},
delay: {
description: 'The delay in milliseconds before the button resets.',
type: 'number',
required: false,
default: '3000',
},
size: {
description: 'The size of the button.',
type: "'default' | 'sm' | 'md' | 'lg'",
required: false,
default: 'default',
},
variant: {
description: 'The variant of the button.',
type: "'default' | 'muted' | 'outline' | 'secondary' | 'ghost'",
required: false,
default: 'default',
},
onCopy: {
description: 'The function to call when copy is successful.',
type: '(content: string) => void',
required: false,
},
}}
/>
## Credits
- Credits to [Shadcn UI](https://ui.shadcn.com/docs/components/button) for the button style.

View File

@ -0,0 +1,71 @@
---
title: Flip Button
description: A clickable button featuring a smooth flipping animation triggered on hover.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="flip-button-demo" />
## Installation
<ComponentInstallation name="flip-button" />
## Usage
```tsx
<FlipButton frontText="Front" backText="Back" />
```
## Examples
### From Left
Use the `from` prop to change the direction of the flip.
<ComponentPreview name="flip-button-from-demo" />
## Props
<TypeTable
type={{
frontText: {
description: 'The text to display on the front of the button',
type: 'string',
required: true,
},
backText: {
description: 'The text to display on the back of the button',
type: 'string',
required: true,
},
transition: {
description: 'The transition of the button',
type: 'Transition',
required: false,
default: '{ type: "spring", stiffness: 280, damping: 20 }',
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
frontClassName: {
description: 'The className of the front of the button',
type: 'string',
required: false,
},
backClassName: {
description: 'The className of the back of the button',
type: 'string',
required: false,
},
from: {
description: 'The direction of the flip',
type: 'string',
required: false,
default: 'top',
},
}}
/>

View File

@ -0,0 +1,70 @@
---
title: GitHub Stars Button
description: A clickable button that links to a GitHub repository and displays the number of stars.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="github-stars-button-demo" />
## Installation
<ComponentInstallation name="github-stars-button" />
## Usage
```tsx
<GitHubStarsButton username="animate-ui" repo="animate-ui" />
```
## Props
<TypeTable
type={{
username: {
description: 'The username of the repository',
type: 'string',
required: true,
},
repo: {
description: 'The repository name',
type: 'string',
required: true,
},
transition: {
description: 'The transition of the component',
type: 'object',
required: false,
default: '{ stiffness: 90, damping: 50 }',
},
formatted: {
description: 'Whether to format the number with a unit (e.g. 18k)',
type: 'boolean',
required: false,
default: 'false',
},
inView: {
description: 'Enable animation when in view.',
type: 'boolean',
required: false,
default: 'false',
},
inViewMargin: {
description: 'The margin of the in view.',
type: 'string',
required: false,
default: '0px',
},
inViewOnce: {
description: 'Enable animation when in view once.',
type: 'boolean',
required: false,
default: 'true',
},
}}
/>
## Credits
- Credits to [SHSF UI](https://www.shsfui.com/primitives/buttons/heart-button) for the icon animation inspiration

View File

@ -0,0 +1,65 @@
---
title: Icon Button
description: An icon button that displays particles when clicked.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="icon-button-demo" />
## Installation
<ComponentInstallation name="icon-button" />
## Usage
```tsx
<IconButton icon={Heart} />
```
## Props
<TypeTable
type={{
icon: {
description: 'The icon to display',
type: 'React.ElementType',
required: true,
},
active: {
description: 'Whether the button is active',
type: 'boolean',
required: false,
default: 'false',
},
animate: {
description: 'Whether the button should display animations',
type: 'boolean',
required: false,
default: 'true',
},
size: {
description: 'The size of the button',
type: "'default' | 'sm' | 'md' | 'lg'",
required: false,
default: 'default',
},
color: {
description: 'The color of the button',
type: '[number, number, number]',
required: false,
default: '[59, 130, 246]',
},
transition: {
description: 'The transition of the button',
type: 'Transition',
required: false,
default: 'spring',
},
}}
/>
## Credits
- Credits to [SHSF UI](https://www.shsfui.com/primitives/buttons/heart-button) for the icon animation inspiration

View File

@ -0,0 +1,79 @@
---
title: Input Button
description: A button that shows an input when clicked.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="input-button-demo" />
## Installation
<ComponentInstallation name="input-button" />
## Usage
```tsx
<InputButton>
<Buttons>
<Button>Button</Button>
<SubmitButton>Submit</SubmitButton>
</Buttons>
<Input />
</InputButton>
```
## Examples
### Newsletter Form with Loading State
<ComponentPreview name="input-button-loading-demo" />
## Props
### InputButton
<TypeTable
type={{
transition: {
description: 'The transition of the component',
type: 'Transition',
required: false,
default: "{ type: 'spring', stiffness: 300, damping: 20 }",
},
showInput: {
description: 'Whether to show the input',
type: 'boolean',
required: false,
default: 'false',
},
setShowInput: {
description: 'The function to set the showInput state',
type: 'React.Dispatch<React.SetStateAction<boolean>>',
required: false,
},
id: {
description: 'The id of the component',
type: 'string',
required: false,
},
}}
/>
### SubmitButton
<TypeTable
type={{
icon: {
description: 'The icon of the component',
type: 'React.ElementType',
required: false,
default: 'ArrowRight',
},
}}
/>
## Credits
- Credits to [Adam Wathan](https://x.com/adamwathan/status/1890105358519341205) for the inspiration

View File

@ -0,0 +1,40 @@
---
title: Liquid Button
description: A clickable button featuring a dynamic, fluid-like animation effect upon hover, creating an engaging, liquid-inspired interaction.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="liquid-button-demo" />
## Installation
<ComponentInstallation name="liquid-button" />
## Usage
```tsx
<LiquidButton>Button</LiquidButton>
```
## Props
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: false,
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
}}
/>
## Credits
- Credits to [Emadamerho Nefe](https://codepen.io/nefejames/pen/yyBedGg) for the inspiration

View File

@ -0,0 +1,53 @@
---
title: Ripple Button
description: A clickable button featuring a ripple animation effect on click.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="ripple-button-demo" />
## Installation
<ComponentInstallation name="ripple-button" />
## Usage
```tsx
<RippleButton>Button</RippleButton>
```
## Props
<TypeTable
type={{
children: {
description: 'The children of the component',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the component',
type: 'string',
required: false,
},
rippleClassName: {
description: 'The className of the ripple',
type: 'string',
required: false,
},
transition: {
description: 'The transition of the ripple',
type: 'Transition',
required: false,
default: '{ duration: 0.6, ease: "easeOut" }',
},
scale: {
description: 'The scale of the ripple',
type: 'number',
required: false,
default: 10,
},
}}
/>

View File

@ -0,0 +1,108 @@
---
title: CLI
description: Use the shadcn CLI to add components to your project.
---
<Callout type="info">
**Note:** We rely on the [shadcn/ui
CLI](https://ui.shadcn.com/docs/installation/cli) to add components to your
project.
</Callout>
## init
Use the `init` command to initialize configuration and dependencies for a new project.
The `init` command installs dependencies, adds the `cn` util and configures CSS variables for the project.
```bash
npx shadcn@latest init
```
### Options
```txt
Usage: shadcn init [options] [components...]
initialize your project and install dependencies
Arguments:
components the components to add or a url to the component.
Options:
-y, --yes skip confirmation prompt. (default: true)
-d, --defaults, use default configuration. (default: false)
-f, --force force overwrite of existing configuration. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory. (default: "/Users/shadcn/Desktop")
-s, --silent mute output. (default: false)
--src-dir use the src directory when creating a new project. (default: false)
--no-src-dir do not use the src directory when creating a new project.
--css-variables use css variables for theming. (default: true)
--no-css-variables do not use css variables for theming.
-h, --help display help for command
```
## add
Use the `add` command to add components and dependencies to your project.
```bash
npx shadcn@latest add [component]
```
### Options
```txt
Usage: shadcn add [options] [components...]
add a component to your project
Arguments:
components the components to add or a url to the component.
Options:
-y, --yes skip confirmation prompt. (default: false)
-o, --overwrite overwrite existing files. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory. (default: "/Users/shadcn/Desktop")
-a, --all add all available components (default: false)
-p, --path <path> the path to add the component to.
-s, --silent mute output. (default: false)
--src-dir use the src directory when creating a new project. (default: false)
--no-src-dir do not use the src directory when creating a new project.
--css-variables use css variables for theming. (default: true)
--no-css-variables do not use css variables for theming.
-h, --help display help for command
```
## build
Use the `build` command to generate the registry JSON files.
```bash
npx shadcn@latest build
```
This command reads the `registry.json` file and generates the registry JSON files in the `public/r` directory.
### Options
```txt
Usage: shadcn build [options] [registry]
build components for a shadcn registry
Arguments:
registry path to registry.json file (default: "./registry.json")
Options:
-o, --output <path> destination directory for json files (default: "./public/r")
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
-h, --help display help for command
```
To customize the output directory, use the `--output` option.
```bash
npx shadcn@latest build --output ./public/registry
```

View File

@ -0,0 +1,214 @@
---
title: Avatar Group
description: An animated avatar group that displays overlapping user images and smoothly shifts each avatar forward on hover to highlight it.
author:
name: imskyleen
url: https://github.com/imskyleen
---
## AvatarGroup
An animated avatar group built with TailwindCSS and Motion, where avatars gently shift forward on hover to highlight each user.
For a CSS-only version with a mask effect that blends seamlessly into any background, check out the [AvatarGroupMask](#avatargroupmask) component.
<ComponentPreview name="avatar-group-demo" />
### Installation
<ComponentInstallation name="avatar-group" />
### Usage
```tsx
<AvatarGroup>
{AVATARS.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
<AvatarGroupTooltip>
<p>{avatar.tooltip}</p>
</AvatarGroupTooltip>
</Avatar>
))}
</AvatarGroup>
```
### Examples
#### Bottom translation
<ComponentPreview name="avatar-group-bottom-demo" />
### Props
#### AvatarGroup
<TypeTable
type={{
children: {
description: 'The avatars to display in the avatar group',
type: 'React.ReactElement[]',
required: true,
},
className: {
description: 'The className of the avatar group',
type: 'string',
required: false,
},
transition: {
description: 'The transition of the avatar group',
type: 'Transition',
required: false,
default: "{ type: 'spring', stiffness: 300, damping: 17 }",
},
invertOverlap: {
description:
'By default, the avatar group is rendered in the same z-index as the avatars. This prop allows you to invert the z-index of the avatar group.',
type: 'boolean',
required: false,
default: false,
},
translate: {
description: 'The translate y of the avatar group',
type: 'string | number',
required: false,
default: '-30%',
},
tooltipProps: {
description: 'The props to pass to the tooltip',
type: 'TooltipProps',
required: false,
default: "{ side: 'top', sideOffset: 20 }",
},
}}
/>
#### AvatarGroupTooltip
<TypeTable
type={{
children: {
description: 'The children of the avatar group tooltip',
type: 'React.ReactNode',
required: true,
},
}}
/>
## AvatarGroupMask
A sleek, CSS-only avatar group using TailwindCSS for the translate animation and a mask effect. Perfect for layered or custom backgrounds — inspired by [Jhey](https://codepen.io/jh3y/pen/yyLmmMW)'s awesome work.
<Callout type="warn">
You need **TailwindCSS 4.1 or higher** to use this component.
</Callout>
<ComponentPreview name="avatar-group-mask-demo" />
### Installation
<ComponentInstallation name="avatar-group-mask" />
### Usage
```tsx
<AvatarGroup>
{AVATARS.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
<AvatarGroupTooltip>
<p>{avatar.tooltip}</p>
</AvatarGroupTooltip>
</Avatar>
))}
</AvatarGroup>
```
### Examples
#### Bottom translation
<ComponentPreview name="avatar-group-mask-bottom-demo" />
<Callout type="info">
**Note:** Children's avatars must have a `data-slot="avatar"` attribute.
</Callout>
### Props
#### AvatarGroup
<TypeTable
type={{
children: {
description: 'The avatars to display in the avatar group',
type: 'React.ReactElement[]',
required: true,
},
className: {
description: 'The className of the avatar group',
type: 'string',
required: false,
},
invertOverlap: {
description:
'By default, mask is rendered in the left side of the avatar group. This prop allows you to invert the mask to the right side.',
type: 'boolean',
required: false,
default: false,
},
size: {
description: 'The size of the avatar group',
type: 'string | number',
required: false,
default: '43px',
},
border: {
description: 'The border of the avatar group',
type: 'string | number',
required: false,
default: '3px',
},
columnSize: {
description: 'The size of the avatar group',
type: 'string | number',
required: false,
default: '37px',
},
align: {
description: 'The alignment of the avatar group',
type: '"start" | "center" | "end"',
required: false,
default: 'end',
},
translate: {
description: 'The translate y of the avatar group',
type: 'number',
required: false,
default: '-30%',
},
tooltipProps: {
description: 'The props to pass to the tooltip',
type: 'TooltipProps',
required: false,
default: "{ side: 'top', sideOffset: 20 }",
},
}}
/>
#### AvatarGroupTooltip
<TypeTable
type={{
children: {
description: 'The children of the avatar group tooltip',
type: 'React.ReactNode',
required: true,
},
}}
/>
## Credits
- Credits to [Jhey](https://codepen.io/jh3y/pen/yyLmmMW) for the inspiration.

View File

@ -0,0 +1,127 @@
---
title: Code Editor
description: A code editor component featuring syntax highlighting and animation.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="code-editor-demo" />
## Installation
<ComponentInstallation name="code-editor" />
## Usage
```tsx
<CodeEditor lang="tsx">
{`const a = 1;
const b = 2;
const c = a + b;`}
</CodeEditor>
```
## Props
<TypeTable
type={{
children: {
description: 'The code to display in the editor.',
type: 'string',
required: true,
},
lang: {
description: 'The language of the code.',
type: 'string',
required: true,
},
themes: {
description:
'The theme of the code editor (see https://shiki.style/themes).',
type: '{ light: string; dark: string }',
required: false,
default: '{ light: "github-light"; dark: "github-dark" }',
},
duration: {
description: 'The duration of the animation.',
type: 'number',
required: false,
default: '5',
},
delay: {
description: 'The delay of the animation.',
type: 'number',
required: false,
default: '0',
},
header: {
description: 'Whether to show the header.',
type: 'boolean',
required: false,
default: 'true',
},
dots: {
description: 'Whether to show the top left dots.',
type: 'boolean',
required: false,
default: 'true',
},
icon: {
description: 'The icon to display in the header.',
type: 'React.ReactNode',
required: false,
},
cursor: {
description: 'Whether to show the cursor.',
type: 'boolean',
required: false,
default: 'false',
},
inView: {
description: 'Enable animation when in view.',
type: 'boolean',
required: false,
default: 'false',
},
inViewMargin: {
description: 'The margin of the in view.',
type: 'string',
required: false,
default: '0px',
},
inViewOnce: {
description: 'Enable animation only once when in view.',
type: 'boolean',
required: false,
default: 'true',
},
copyButton: {
description: 'Whether to show the copy button.',
type: 'boolean',
required: false,
default: 'false',
},
writing: {
description: 'Enable writing animation.',
type: 'boolean',
required: false,
default: 'true',
},
title: {
description: 'The file name.',
type: 'string',
required: false,
},
onDone: {
description: 'Callback when the animation is done.',
type: '() => void',
required: false,
},
onCopy: {
description: 'The function to call when copy is successful.',
type: '(content: string) => void',
required: false,
},
}}
/>

View File

@ -0,0 +1,65 @@
---
title: Code Tabs
description: A tabs component that displays code for different languages.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<Callout type="info">
The InstallTabs component has been updated and is now called CodeTabs. It can
now be used to make both install tabs and code tabs!
</Callout>
<ComponentPreview name="code-tabs-demo" />
## Installation
<ComponentInstallation name="code-tabs" />
## Usage
```tsx
<CodeTabs codes={codes} />
```
## Examples
### Install Tabs
<ComponentPreview name="install-tabs-demo" />
## Props
<TypeTable
type={{
codes: {
description: 'The codes to display in the tabs',
type: 'Record<string, string>',
required: true,
},
lang: {
description: 'The language of the code',
type: 'string',
required: false,
default: 'bash',
},
themes: {
description: 'The shiki themes to display in the tabs',
type: 'Record<string, string>',
required: false,
default: "{ light: 'github-light', dark: 'github-dark' }",
},
copyButton: {
description: 'Whether to show the copy button.',
type: 'boolean',
required: false,
default: 'true',
},
onCopy: {
description: 'The function to call when copy is successful.',
type: '(content: string) => void',
required: false,
},
}}
/>

View File

@ -0,0 +1,57 @@
---
title: Counter
description: A numeric input control featuring increment and decrement buttons, smoothly animating number transitions using the SlidingNumber component.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="counter-demo" />
## Installation
<ComponentInstallation name="counter" />
## Usage
```tsx
<Counter number={number} setNumber={setNumber} />
```
## Props
<TypeTable
type={{
buttonProps: {
description: 'The props to pass to the Button component',
type: "Omit<React.ComponentProps<typeof Button>, 'onClick'>",
required: false,
},
className: {
description: 'The className to pass to the component',
type: 'string',
required: false,
},
number: {
description: 'The number to display',
type: 'number',
required: true,
},
setNumber: {
description: 'The function to set the number',
type: '(number: number) => void',
required: true,
},
slidingNumberProps: {
description: 'The props to pass to the SlidingNumber component',
type: "Omit<SlidingNumberProps, 'number'>",
required: false,
},
transition: {
description: 'The transition of the counter width',
type: 'Transition',
required: false,
default: '{ type: "spring", bounce: 0, stiffness: 300, damping: 30 }',
},
}}
/>

View File

@ -0,0 +1,90 @@
---
title: Cursor
description: An animated cursor component that allows you to customize both the cursor and cursor follow elements with smooth animations.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="cursor-demo" />
## Installation
<ComponentInstallation name="cursor" />
## Usage
```tsx
<CursorProvider>
<Cursor>
<svg>{...}</svg>
</Cursor>
<CursorFollow>
<div>Follow me</div>
</CursorFollow>
</CursorProvider>
```
## Examples
### Cursor Follow Only
You can use only the `CursorFollow` (like the example below) component or the `Cursor` component.
<ComponentPreview name="cursor-follow-only-demo" />
## Props
### CursorProvider
<TypeTable
type={{
children: {
description: 'The children of the cursor provider',
type: 'React.ReactNode',
required: true,
},
}}
/>
### Cursor
<TypeTable
type={{
children: {
description: 'The children of the cursor',
type: 'React.ReactNode',
required: true,
},
}}
/>
### CursorFollow
<TypeTable
type={{
children: {
description: 'The children of the cursor follow',
type: 'React.ReactNode',
required: true,
},
sideOffset: {
description: 'The side offset of the cursor follow',
type: 'number',
required: false,
default: 15,
},
align: {
description: 'The align of the cursor follow',
type: "'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right' | 'left' | 'right' | 'center'",
required: false,
default: 'bottom-right',
},
transition: {
description: 'The transition of the cursor follow',
type: 'SpringOptions',
required: false,
default: '{ stiffness: 500, damping: 50, bounce: 0 }',
},
}}
/>

View File

@ -0,0 +1,137 @@
---
title: Files
description: A component that allows you to display a list of files and folders.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="files-demo" />
## Installation
<ComponentInstallation name="files" />
## Usage
```tsx
<Files>
<Folder name="Folder 1">
<File name="file1.txt" />
<File name="file2.txt" />
</Folder>
<Folder name="Folder 2">
<File name="file3.txt" />
<File name="file4.txt" />
</Folder>
</Files>
```
## Examples
### Files Advanced
<ComponentPreview name="files-advanced-demo" />
## Props
### Files
<TypeTable
type={{
children: {
description: 'The children of the component.',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The class name of the component.',
type: 'string',
required: false,
},
activeClassName: {
description: 'The class name of the highlight.',
type: 'string',
required: false,
},
defaultOpen: {
description: 'The child folders open by default.',
type: 'string[]',
required: false,
},
open: {
description: 'The child folders open.',
type: 'string[]',
required: false,
},
onOpenChange: {
description: 'The callback function when the child folders open change.',
type: 'function',
required: false,
},
sideComponent: {
description:
'The component to display on the right side of the component.',
type: 'React.ReactNode',
required: false,
},
}}
/>
### Folder
<TypeTable
type={{
name: {
description: 'The name of the folder.',
type: 'string',
required: true,
},
children: {
description: 'The children of the component.',
type: 'React.ReactNode',
required: false,
},
className: {
description: 'The class name of the component.',
type: 'string',
required: false,
},
defaultOpen: {
description: 'The child folders open by default.',
type: 'string[]',
required: false,
},
open: {
description: 'The child folders open.',
type: 'string[]',
required: false,
},
onOpenChange: {
description: 'The callback function when the child folders open change.',
type: 'function',
required: false,
},
}}
/>
### File
<TypeTable
type={{
className: {
description: 'The class name of the component.',
type: 'string',
required: false,
},
name: {
description: 'The name of the file.',
type: 'string',
required: true,
},
}}
/>
## Credits
- Credits to [Fumadocs](https://fumadocs.vercel.app/docs/ui/components/files) for the inspiration

View File

@ -0,0 +1,65 @@
---
title: Liquid Glass
description: A component that allows you to display a liquid glass effect, inspired by iOS26.
author:
name: lucasporrini
url: https://github.com/lucasporrini
new: true
---
<ComponentPreview name="liquid-glass-demo" />
## Installation
<ComponentInstallation name="liquid-glass" />
## Usage
```tsx
<LiquidGlass>Hello world</LiquidGlass>
```
## Props
<TypeTable
type={{
as: {
description: 'The element or component the liquid glass should render as',
type: 'String | Component',
default: 'div',
required: false,
},
radius: {
description: 'The radius of the liquid glass',
type: 'Number',
default: '25',
required: false,
},
blur: {
description: 'The blur of the liquid glass background',
type: 'Number',
default: '0',
required: false,
},
childClassName: {
description: 'The class name of the child element',
type: 'String',
required: false,
},
className: {
description: 'The class name of the liquid glass',
type: 'String',
required: false,
},
children: {
description: 'The children of the liquid glass',
type: 'React.ReactNode',
required: true,
},
}}
/>
## Credits
- Credits to [Lucas Porrini](https://github.com/lucasporrini) for creating the component
- Credits to [@rebane2001](https://x.com/rebane2001/status/1932451851523264749?s=46&t=SBwgWoAagRgIPNE-RF4R9w) for effect inspiration

View File

@ -0,0 +1,76 @@
---
title: Motion Grid
description: A grid that displays animations in a grid.
author:
name: imskyleen
url: https://github.com/imskyleen
new: true
---
import { MotionGridEditor } from '../../../components/docs/motion-grid-editor';
<ComponentPreview name="motion-grid-demo" />
## Installation
<ComponentInstallation name="motion-grid" />
## Usage
```tsx
<MotionGrid gridSize={[5, 5]} frames={frames} />
```
## Editor
<MotionGridEditor />
## Props
### MotionGrid
<TypeTable
type={{
gridSize: {
description: 'The size of the grid as [rows, columns].',
type: '[number, number]',
required: true,
},
frames: {
description: 'Array of frame data for the grid.',
type: '[number, number][][]',
required: true,
},
duration: {
description: 'The duration of the animation in milliseconds.',
type: 'number',
required: false,
default: '200',
},
animate: {
description: 'Whether to animate the grid.',
type: 'boolean',
required: false,
default: 'true',
},
cellClassName: {
description: 'Class name for the cells.',
type: 'string',
required: false,
},
cellActiveClassName: {
description: 'Class name for the active cell.',
type: 'string',
required: false,
},
cellInactiveClassName: {
description: 'Class name for the inactive cell.',
type: 'string',
required: false,
},
}}
/>
## Credits
- Credits to [Adrien Griveau](https://x.com/Griveau) for [this X post](https://x.com/Griveau/status/1932832554354163804) as inspiration

View File

@ -0,0 +1,88 @@
---
title: Pin List
description: A playful list for pinning and unpinning items, with smooth animated transitions as items move between groups.
author:
name: arhamkhnz
url: https://github.com/arhamkhnz
new: true
---
<ComponentPreview name="pin-list-demo" />
## Installation
<ComponentInstallation name="pin-list" />
## Usage
```tsx
<PinList items={ITEMS} />
```
## Props
### PinList
<TypeTable
type={{
items: {
description:
'Array of items to show in the list. Each item should have id, name, info, icon, and pinned.',
type: 'PinListItem[]',
required: true,
},
labels: {
description:
'Custom labels for the pinned and unpinned sections. Example: { pinned: "Favorites", unpinned: "All" }.',
type: '{ pinned?: string; unpinned?: string; }',
required: false,
default: '{ pinned: "Pinned Items", unpinned: "All Items" }',
},
transition: {
description:
'Spring animation config for item transitions. Controls the movement physics. Example: { stiffness: 320, damping: 20, mass: 0.8, type: "spring" }',
type: '{ stiffness?: number; damping?: number; mass?: number; type?: string; }',
required: false,
default: '{ stiffness: 320, damping: 20, mass: 0.8, type: "spring" }',
},
labelMotionProps: {
description:
'Motion props for the section labels. Allows customizing the label enter/exit animation. Example: { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.22, ease: "easeInOut" } }',
type: '{ initial?: object; animate?: object; exit?: object; transition?: object; }',
required: false,
default:
'{ initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.22, ease: "easeInOut" } }',
},
className: {
description: 'Class name for the outermost container.',
type: 'string',
required: false,
},
labelClassName: {
description: 'Class name for the section labels.',
type: 'string',
required: false,
},
pinnedSectionClassName: {
description: 'Class name for the pinned items group.',
type: 'string',
required: false,
},
unpinnedSectionClassName: {
description: 'Class name for the unpinned items group.',
type: 'string',
required: false,
},
zIndexResetDelay: {
description:
'Delay (in ms) before resetting z-index after toggle, should match your animation duration if changed.',
type: 'number',
required: false,
default: '500',
},
}}
/>
## Credits
- Credits to [Nitish Khagwal](https://x.com/nitishkmrk) for [this X post](https://x.com/nitishkmrk/status/1933050634594660800) as inspiration

View File

@ -0,0 +1,50 @@
---
title: Scroll Progress
description: A scroll progress component that displays the progress of the scroll.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="scroll-progress-demo" />
## Installation
<ComponentInstallation name="scroll-progress" />
## Usage
### Global Scroll Progress
```tsx
<ScrollProgress />
```
### Local Scroll Progress
```tsx
<ScrollProgress progressProps={{ className: 'absolute' }}>...</ScrollProgress>
```
## Props
<TypeTable
type={{
progressProps: {
description: 'The props for the progress element',
type: 'HTMLMotionProps<"div">',
required: false,
},
className: {
description: 'The class name for the scroll progress',
type: 'string',
required: false,
},
children: {
description:
'The children for the scroll progress (if not provided, the progress will be global)',
type: 'React.ReactNode',
required: false,
},
}}
/>

View File

@ -0,0 +1,82 @@
---
title: Spring Element
description: A flexible, animated spring component that attaches a draggable element (avatar, text, icon, or any React node) to its origin with a spring line.
author:
name: arhamkhnz
url: https://github.com/arhamkhnz
---
<ComponentPreview name="spring-element-demo" />
## Installation
<ComponentInstallation name="spring-element" />
## Usage
```tsx
<SpringElement>
<Avatar className="size-12">
<AvatarImage draggable={false} src={USER.src} />
<AvatarFallback>{USER.fallback}</AvatarFallback>
</Avatar>
</SpringElement>
```
<Callout type="info">
**Note:** Add <code>draggable=&#123;false&#125;</code> to your element
(avatar, image, etc.) when passing it as children. This prevents the browser's
native drag image from interfering with the spring drag animation.
</Callout>
## Props
### SpringElement
<TypeTable
type={{
children: {
description:
'The element to attach the spring to. Can be any React node (avatar, text, icon, etc.).',
type: 'React.ReactElement',
required: true,
},
className: {
description: 'ClassName for the element.',
type: 'string',
required: false,
},
springClassName: {
description:
'ClassName for the spring path (to control stroke color, thickness, etc.). Default is "stroke-2 stroke-neutral-900 fill-none".',
type: 'string',
required: false,
},
dragElastic: {
description:
'How elastic the drag interaction feels. Lower values = less stretch. (Framer Motion prop)',
type: 'number',
required: false,
default: '0.4',
},
springConfig: {
description:
'Spring physics config for the element motion. (stiffness, damping)',
type: '{ stiffness?: number; damping?: number }',
required: false,
default: '{ stiffness: 200, damping: 16 }',
},
springPathConfig: {
description:
'Config for the spring path itself: number of coils, amplitude, curve ratios, etc.',
type: '{ coilCount?: number; amplitudeMin?: number; amplitudeMax?: number; curveRatioMin?: number; curveRatioMax?: number; bezierOffset?: number; }',
required: false,
default:
'{ coilCount: 8, amplitudeMin: 8, amplitudeMax: 20, curveRatioMin: 0.5, curveRatioMax: 1, bezierOffset: 8 }',
},
}}
/>
## Credits
- Credits to [Anh](https://x.com/pwign) for [this X post](https://x.com/pwign/status/1927760004183970067) as inspiration

View File

@ -0,0 +1,80 @@
---
title: Stars Scrolling Wheel
description: A scrolling wheel that displays stars count.
author:
name: imskyleen
url: https://github.com/imskyleen
new: true
---
<ComponentPreview name="stars-scrolling-wheel-demo" />
## Installation
<ComponentInstallation name="stars-scrolling-wheel" />
## Usage
```tsx
<StarsScrollingWheel stars={1000} />
```
## Props
<TypeTable
type={{
stars: {
description: 'The number of stars to display',
type: 'number',
required: true,
},
step: {
description: 'The step of the stars',
type: 'number',
required: false,
default: '100',
},
itemHeight: {
description: 'The height of the item',
type: 'number',
required: false,
default: '48',
},
sideItemsCount: {
description: 'The number of side items',
type: 'number',
required: false,
default: '2',
},
transition: {
description: 'The spring transition of the stars scrolling wheel',
type: 'SpringOptions',
required: false,
default: '{ stiffness: 90, damping: 30 }',
},
inView: {
description: 'Whether the component is in view',
type: 'boolean',
required: false,
default: 'false',
},
inViewOnce: {
description: 'Whether the component should only animate once',
type: 'boolean',
required: false,
default: 'true',
},
inViewMargin: {
description: 'The margin of the component',
type: 'string',
required: false,
default: '0px',
},
delay: {
description: 'The delay of the component',
type: 'number',
required: false,
default: '0',
},
}}
/>

View File

@ -0,0 +1,157 @@
---
title: Tabs
description: A set of layered sections with the same height of content—known as tab panels—that are displayed one at a time.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="tabs-demo" />
## Installation
<ComponentInstallation name="tabs" />
## Usage
```tsx
<Tabs>
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContents>
<TabsContent value="tab1">Content 1</TabsContent>
<TabsContent value="tab2">Content 2</TabsContent>
</TabsContents>
</Tabs>
```
## Props
### Tabs
<TypeTable
type={{
children: {
description: 'The children of the tabs',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the tabs',
type: 'string',
required: false,
},
defaultValue: {
description: 'The default value of the tabs',
type: 'string',
required: false,
},
onValueChange: {
description: 'The function to handle the value change',
type: '(value: string) => void',
required: false,
},
value: {
description: 'The value of the tabs',
type: 'string',
required: false,
},
}}
/>
### TabsList
<TypeTable
type={{
activeClassName: {
description: 'The className of the active tab',
type: 'string',
required: false,
},
children: {
description: 'The children of the tabs list',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the tabs list',
type: 'string',
required: false,
},
transition: {
description: 'The transition of the tabs list',
type: 'Transition',
required: false,
default: '{ type: "spring", stiffness: 200, damping: 25 }',
},
}}
/>
### TabsTrigger
<TypeTable
type={{
children: {
description: 'The children of the tabs trigger',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the tabs trigger',
type: 'string',
required: false,
},
value: {
description: 'The value of the tabs trigger',
type: 'string',
required: true,
},
}}
/>
### TabsContents
<TypeTable
type={{
children: {
description: 'The children of the tabs contents',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the tabs contents',
type: 'string',
required: false,
},
transition: {
description: 'The transition of the tabs contents',
type: 'Transition',
required: false,
default: '{ duration: 0.4, ease: "easeInOut" }',
},
}}
/>
### TabsContent
<TypeTable
type={{
children: {
description: 'The children of the tabs content',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the tabs content',
type: 'string',
required: false,
},
value: {
description: 'The value of the tabs content',
type: 'string',
required: true,
},
}}
/>

View File

@ -0,0 +1,128 @@
---
title: Tooltip
description: An animated tooltip that shows contextual info on hover or focus and smoothly glides to the next element without disappearing between transitions.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="tooltip-demo" />
## Installation
<ComponentInstallation name="tooltip" />
## Usage
```tsx
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<span>Hover me</span>
</TooltipTrigger>
<TooltipContent>Tooltip content</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<span>And me</span>
</TooltipTrigger>
<TooltipContent>Tooltip content</TooltipContent>
</Tooltip>
</TooltipProvider>
```
## Props
### TooltipProvider
<TypeTable
type={{
children: {
description: 'The children of the tooltip provider',
type: 'React.ReactNode',
required: true,
},
openDelay: {
description: 'The delay in milliseconds before the tooltip is shown',
type: 'number',
required: false,
default: 700,
},
closeDelay: {
description: 'The delay in milliseconds before the tooltip is hidden',
type: 'number',
required: false,
default: 300,
},
transition: {
description: 'The transition of the tooltip',
type: 'Transition',
required: false,
default: "{ type: 'spring', stiffness: 300, damping: 25 }",
},
}}
/>
### Tooltip
<TypeTable
type={{
children: {
description: 'The children of the tooltip',
type: 'React.ReactNode',
required: true,
},
side: {
description: 'The side of the tooltip',
type: '"top" | "bottom" | "left" | "right"',
required: false,
default: 'top',
},
sideOffset: {
description: 'The offset of the tooltip',
type: 'number',
required: false,
default: 4,
},
align: {
description: 'The alignment of the tooltip',
type: '"start" | "center" | "end"',
required: false,
default: 'center',
},
alignOffset: {
description: 'The offset of the tooltip',
type: 'number',
required: false,
default: 0,
},
}}
/>
### TooltipContent
<TypeTable
type={{
children: {
description: 'The children of the tooltip content',
type: 'React.ReactNode',
required: true,
},
}}
/>
### TooltipTrigger
<TypeTable
type={{
children: {
description: 'The children of the tooltip trigger',
type: 'React.ReactElement',
required: true,
},
}}
/>
## Credits
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/tooltip) for the tooltip style.

View File

@ -0,0 +1,64 @@
---
title: Magnetic
description: A magnetic effect that clings to the cursor, creating a magnetic attraction effect.
author:
name: imskyleen
url: https://github.com/imskyleen
new: true
---
<ComponentPreview name="magnetic-demo" />
## Installation
<ComponentInstallation name="magnetic" />
## Usage
```tsx
<Magnetic>
<p>Magnetic</p>
</Magnetic>
```
## Props
<TypeTable
type={{
children: {
description: 'The children of the magnetic',
type: 'React.ReactElement',
required: true,
},
disableOnTouch: {
description: 'Whether the magnetic effect should be disabled on touch',
type: 'boolean',
required: false,
defaultValue: 'true',
},
onlyOnHover: {
description: 'Whether the magnetic effect should only be active on hover',
type: 'boolean',
required: false,
defaultValue: 'false',
},
strength: {
description: 'The strength of the magnetic effect',
type: 'number',
required: false,
defaultValue: '0.5',
},
range: {
description: 'The range of the magnetic effect',
type: 'number',
required: false,
defaultValue: '120',
},
springOptions: {
description: 'The spring options of the magnetic effect',
type: 'SpringOptions',
required: false,
defaultValue: '{ stiffness: 100, damping: 10, mass: 0.5 }',
},
}}
/>

View File

@ -0,0 +1,106 @@
---
title: Motion Effect
description: Motion effect component that displays the motion effect (fade in, slide in, etc.).
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="motion-effect-image-grid-demo" />
## Installation
<ComponentInstallation name="motion-effect" />
## Usage
```tsx
<MotionEffect slide>
<p>Motion Effect</p>
</MotionEffect>
```
## Examples
### Slide
<ComponentPreview name="motion-effect-slide-demo" />
### Fade Blur
<ComponentPreview name="motion-effect-fade-blur-demo" />
## Props
<TypeTable
type={{
children: {
description: 'The children of the slide in',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the slide in',
type: 'string',
required: false,
},
transition: {
description: 'The transition of the slide in',
type: 'Transition',
required: false,
default: "{ type: 'spring', stiffness: 200, damping: 20 }",
},
inView: {
description: 'Whether the slide in is in view',
type: 'boolean',
required: false,
default: 'false',
},
inViewMargin: {
description: 'The margin of the slide in when it is in view',
type: 'string',
required: false,
default: '0px',
},
inViewOnce: {
description: 'Whether the slide in is in view once',
type: 'boolean',
required: false,
default: 'true',
},
delay: {
description: 'The delay of the slide in',
type: 'number',
required: false,
default: '0',
},
blur: {
description: 'The blur of the slide in',
type: 'boolean | string',
required: false,
default: 'false',
},
slide: {
description: 'The slide of the slide in',
type: "boolean | { direction?: 'up' | 'down' | 'left' | 'right'; offset?: number }",
required: false,
default: 'false',
},
fade: {
description: 'The fade of the slide in',
type: 'boolean | { initialOpacity?: number; opacity?: number }',
required: false,
default: 'false',
},
zoom: {
description: 'The zoom of the slide in',
type: 'boolean | { initialScale?: number; scale?: number }',
required: false,
default: 'false',
},
}}
/>
## Credits
- Credits to [Magic UI](https://magicui.design/docs/components/blur-fade) for the inspiration

View File

@ -0,0 +1,345 @@
---
title: Motion Highlight
description: Motion highlight component that displays the motion highlight effect.
author:
name: imskyleen
url: https://github.com/imskyleen
---
<ComponentPreview name="motion-highlight-cards-hover-demo" />
## Installation
<ComponentInstallation name="motion-highlight" />
## Usage
```tsx
<MotionHighlight>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</MotionHighlight>
```
## Examples
### Tabs
<ComponentPreview name="motion-highlight-tabs-demo" />
### Tabs Hover
<ComponentPreview name="motion-highlight-tabs-hover-demo" />
### Tabs Hover with Parent Mode
<ComponentPreview name="motion-highlight-tabs-hover-parent-demo" />
## Usage
### Uncontrolled Items
By default, child items will have the highlight effect. This will map the children elements and surround them with the `MotionHighlightItem` element.
```tsx
<MotionHighlight>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</MotionHighlight>
```
### Controlled Items
Using the `controlledItems` props, you place your `MotionHighlightItem` wherever you like in the `MotionHighlight` component.
```tsx
<MotionHighlight controlledItems>
<MotionHighlightItem>Item 1</MotionHighlightItem>
<div>
<MotionHighlightItem>Item 2</MotionHighlightItem>
</div>
<MotionHighlightItem>Item 3</MotionHighlightItem>
</MotionHighlight>
```
### Children Mode
By default, in `mode` with the value `children` the highlight is placed in the `MotionHighlightItem` component using Motion's `layoutId` technique. This technique should be preferred in most cases. However, in some cases, you may run into z-index problems, so it's best to use the `parent` mode to avoid visual bugs.
```tsx
<MotionHighlight>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</MotionHighlight>
```
### Parent Mode
By using the `mode` property with the value `parent`, the `MotionHighlight` component will set the highlight to absolute in `MotionHighlight`. This may be useful in some cases to avoid the visual z-index bugs you can get with `mode` `children`.
```tsx
<MotionHighlight mode="parent">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</MotionHighlight>
```
### MotionHighlightItem asChild
#### Children Mode
```tsx
// With asChild
<MotionHighlight>
<MotionHighlightItem asChild>
<div id="item-1">Item 1</div>
</MotionHighlightItem>
<MotionHighlightItem asChild>
<div id="item-2">Item 2</div>
</MotionHighlightItem>
</MotionHighlight>
// Result
<div className="relative" id="item-1">
<div /> {/* Highlight */}
<div className="relative z-[1]">Item 1</div>
</div>
<div className="relative" id="item-2">
<div /> {/* Highlight */}
<div className="relative z-[1]">Item 2</div>
</div>
// Without asChild
<MotionHighlight>
<MotionHighlightItem>
<div id="item-1">Item 1</div>
</MotionHighlightItem>
<MotionHighlightItem>
<div id="item-2">Item 2</div>
</MotionHighlightItem>
</MotionHighlight>
// Result
<div className="relative" >
<div /> {/* Highlight */}
<div className="relative z-[1]" id="item-1">Item 1</div>
</div>
<div className="relative" >
<div /> {/* Highlight */}
<div className="relative z-[1]" id="item-2">Item 2</div>
</div>
```
#### Parent Mode
```tsx
// With asChild
<MotionHighlight mode="parent">
<MotionHighlightItem asChild>
<div id="item-1">Item 1</div>
</MotionHighlightItem>
<MotionHighlightItem asChild>
<div id="item-2">Item 2</div>
</MotionHighlightItem>
</MotionHighlight>
// Result
<div className="relative">
<div /> {/* Highlight */}
<div id="item-1">Item 1</div>
<div id="item-2">Item 2</div>
</div>
// Without asChild
<MotionHighlight mode="parent">
<MotionHighlightItem>
<div id="item-1">Item 1</div>
</MotionHighlightItem>
<MotionHighlightItem>
<div id="item-2">Item 2</div>
</MotionHighlightItem>
</MotionHighlight>
// Result
<div className="relative">
<div /> {/* Highlight */}
<div>
<div id="item-1">Item 1</div>
</div>
<div>
<div id="item-2">Item 2</div>
</div>
</div>
```
## Props
### MotionHighlight
<TypeTable
type={{
children: {
description: 'The children of the motion highlight',
type: 'React.ReactElement | React.ReactElement[]',
required: true,
},
children: {
description: 'The children of the motion highlight',
type: 'React.ReactNode',
required: true,
},
className: {
description: 'The className of the motion highlight',
type: 'string',
required: false,
},
containerClassName: {
description:
'The className of the container of the motion highlight (only used when mode is "parent")',
type: 'string',
required: false,
},
itemsClassName: {
description:
'The className of the items of the motion highlight (only used when controlledItems is true)',
type: 'string',
required: false,
},
controlledItems: {
description: 'Whether the motion highlight is controlled',
type: 'boolean',
required: false,
default: 'false',
},
value: {
description: 'The value of the motion highlight',
type: 'string | null',
required: false,
},
defaultValue: {
description: 'The default value of the motion highlight',
type: 'string | null',
required: false,
},
onValueChange: {
description: 'The function to call when the value changes',
type: '(value: string | null) => void',
required: false,
},
transition: {
description: 'The transition of the motion highlight',
type: 'Transition',
required: false,
default: '{ type: "spring", stiffness: 200, damping: 25 }',
},
hover: {
description: 'Whether the motion highlight is on hover',
type: 'boolean',
required: false,
default: 'false',
},
enabled: {
description: 'Whether the motion highlight is enabled',
type: 'boolean',
required: false,
default: 'true',
},
disabled: {
description: 'Whether the motion highlight items is disabled',
type: 'boolean',
required: false,
default: 'false',
},
exitDelay: {
description: 'The delay of the exit of the motion highlight items',
type: 'number',
required: false,
default: '0.2',
},
mode: {
description: 'The mode of the motion highlight',
type: "'children' | 'parent'",
required: false,
default: 'children',
},
boundsOffset: {
description:
'The offset of the bounds of the motion highlight (only used when mode is "parent"). For example, you need to add an offset of the same size as the border when setting a border in containerClassName.',
type: 'Partial<Bounds>',
required: false,
default: 'undefined',
},
forceUpdateBounds: {
description:
'Whether to force update the bounds of the motion highlight items (only used when mode is "parent")',
type: 'boolean',
required: false,
default: 'undefined',
},
}}
/>
### MotionHighlightItem
<TypeTable
type={{
children: {
description: 'The children of the motion highlight item',
type: 'React.ReactElement',
required: true,
},
className: {
description: 'The className of the motion highlight item',
type: 'string',
required: false,
},
activeClassName: {
description: 'The className of the active motion highlight item',
type: 'string',
required: false,
},
transition: {
description: 'The transition of the motion highlight item',
type: 'Transition',
required: false,
},
id: {
description: 'The id of the motion highlight item',
type: 'string',
required: false,
},
value: {
description: 'The value of the motion highlight item',
type: 'string',
required: false,
},
disabled: {
description: 'Whether the motion highlight item is disabled',
type: 'boolean',
required: false,
default: 'false',
},
exitDelay: {
description: 'The delay of the exit of the motion highlight item',
type: 'number',
required: false,
},
asChild: {
description: 'Whether to use the child component as the component',
type: 'boolean',
required: false,
default: 'false',
},
forceUpdateBounds: {
description:
'Whether to force update the bounds of the motion highlight item (only used when mode is "parent")',
type: 'boolean',
required: false,
default: 'undefined',
},
}}
/>
## Credits
- Credits to [motion-primitives](https://motion-primitives.com/docs/animated-background) for the inspiration

Some files were not shown because too many files have changed in this diff Show More