Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding terminal component #515

Merged
merged 5 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions __registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: [],
},
terminal: {
name: "terminal",
type: "registry:ui",
registryDependencies: undefined,
files: ["registry/default/magicui/terminal.tsx"],
component: React.lazy(
() => import("@/registry/default/magicui/terminal.tsx"),
),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: [],
},
"aurora-text": {
name: "aurora-text",
type: "registry:ui",
Expand Down Expand Up @@ -880,6 +893,19 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: [],
},
"terminal-demo": {
name: "terminal-demo",
type: "registry:example",
registryDependencies: ["terminal"],
files: ["registry/default/example/terminal-demo.tsx"],
component: React.lazy(
() => import("@/registry/default/example/terminal-demo.tsx"),
),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: [],
},
"morphing-text-demo": {
name: "morphing-text-demo",
type: "registry:example",
Expand Down
6 changes: 6 additions & 0 deletions config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ export const docsConfig: DocsConfig = {
href: `/docs/components/marquee`,
items: [],
},
{
title: "Terminal",
href: `/docs/components/terminal`,
items: [],
label: "New",
},
{
title: "Hero Video Dialog",
href: `/docs/components/hero-video-dialog`,
Expand Down
66 changes: 66 additions & 0 deletions content/docs/components/terminal.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Terminal
date: 2025-01-16
description: An implementation of the MacOS terminal. Useful for showcasing a command line interface.
author: dillionverma
published: true
---

<ComponentPreview name="terminal-demo" />

## Installation

<Tabs defaultValue="cli">

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

```bash
npx shadcn@latest add "https://magicui.design/r/terminal"
```

</TabsContent>

<TabsContent value="manual">

<Steps>

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

<ComponentSource name="terminal" />

</Steps>

</TabsContent>

</Tabs>

## Props

### Terminal

| Prop | Type | Default | Description |
| ----------- | ----------- | ------- | ---------------------------------------- |
| `children` | `ReactNode` | `-` | Content to be typed out in the terminal. |
| `className` | `string` | `-` | Custom CSS class for styling. |

### AnimatedSpan

| Prop | Type | Default | Description |
| ----------- | ----------- | ------- | -------------------------------------------------- |
| `children` | `ReactNode` | `-` | Content to be animated. |
| `delay` | `number` | `0` | Delay in milliseconds before the animation starts. |
| `className` | `string` | `-` | Custom CSS class for styling. |

### TypingAnimation

| Prop | Type | Default | Description |
| ----------- | ------------------- | -------- | -------------------------------------------------- |
| `children` | `ReactNode` | `-` | Content to be animated. |
| `delay` | `number` | `0` | Delay in milliseconds before the animation starts. |
| `className` | `string` | `-` | Custom CSS class for styling. |
| `duration` | `number` | `100` | Duration in milliseconds for each character typed. |
| `as` | `React.ElementType` | `"span"` | The component type to render. |
13 changes: 13 additions & 0 deletions public/r/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@
}
]
},
{
"name": "terminal",
"type": "registry:ui",
"dependencies": [
"motion"
],
"files": [
{
"path": "magicui/terminal.tsx",
"type": "registry:ui"
}
]
},
{
"name": "aurora-text",
"type": "registry:ui",
Expand Down
15 changes: 15 additions & 0 deletions public/r/styles/default/terminal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "terminal",
"type": "registry:ui",
"dependencies": [
"motion"
],
"files": [
{
"path": "magicui/terminal.tsx",
"content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, MotionProps } from \"motion/react\";\nimport { useEffect, useState, useRef } from \"react\";\n\ninterface AnimatedSpanProps {\n children: React.ReactNode;\n delay?: number;\n className?: string;\n}\n\nexport const AnimatedSpan = ({\n children,\n delay = 0,\n className,\n}: AnimatedSpanProps) => (\n <motion.div\n initial={{ opacity: 0, y: -5 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ duration: 0.3, delay: delay / 1000 }}\n className={cn(\"grid text-sm font-normal tracking-tight\", className)}\n >\n {children}\n </motion.div>\n);\n\ninterface TypingAnimationProps extends MotionProps {\n children: string;\n className?: string;\n duration?: number;\n delay?: number;\n as?: React.ElementType;\n}\n\nexport const TypingAnimation = ({\n children,\n className,\n duration = 100,\n delay = 0,\n as: Component = \"span\",\n ...props\n}: TypingAnimationProps) => {\n const MotionComponent = motion.create(Component, {\n forwardMotionProps: true,\n });\n\n const [displayedText, setDisplayedText] = useState<string>(\"\");\n const [started, setStarted] = useState(false);\n const elementRef = useRef<HTMLElement | null>(null);\n\n useEffect(() => {\n const startTimeout = setTimeout(() => {\n setStarted(true);\n }, delay);\n return () => clearTimeout(startTimeout);\n }, [delay]);\n\n useEffect(() => {\n if (!started) return;\n\n let i = 0;\n const typingEffect = setInterval(() => {\n if (i < children.length) {\n setDisplayedText(children.substring(0, i + 1));\n i++;\n } else {\n clearInterval(typingEffect);\n }\n }, duration);\n\n return () => {\n clearInterval(typingEffect);\n };\n }, [children, duration, started]);\n\n return (\n <MotionComponent\n ref={elementRef}\n className={cn(\"text-sm font-normal tracking-tight\", className)}\n {...props}\n >\n {displayedText}\n </MotionComponent>\n );\n};\n\ninterface TerminalProps {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const Terminal = ({ children, className }: TerminalProps) => {\n return (\n <div\n className={cn(\n \"z-0 min-h-[300px] w-full max-w-lg rounded-xl border border-border bg-background\",\n className,\n )}\n >\n <div className=\"flex flex-col gap-y-2 border-b border-border p-4\">\n <div className=\"flex flex-row gap-x-2\">\n <div className=\"h-2 w-2 rounded-full bg-red-500\"></div>\n <div className=\"h-2 w-2 rounded-full bg-yellow-500\"></div>\n <div className=\"h-2 w-2 rounded-full bg-green-500\"></div>\n </div>\n </div>\n <pre className=\"p-4\">\n <code className=\"grid gap-y-2\">{children}</code>\n </pre>\n </div>\n );\n};\n",
"type": "registry:ui",
"target": ""
}
]
}
62 changes: 62 additions & 0 deletions registry/default/example/terminal-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
AnimatedSpan,
Terminal,
TypingAnimation,
} from "@/registry/default/magicui/terminal";

export default function TerminalDemo() {
return (
<Terminal>
<TypingAnimation>&gt; pnpm dlx shadcn@latest init</TypingAnimation>

<AnimatedSpan delay={1500} className="text-green-500">
<span>✔ Preflight checks.</span>
</AnimatedSpan>

<AnimatedSpan delay={2000} className="text-green-500">
<span>✔ Verifying framework. Found Next.js.</span>
</AnimatedSpan>

<AnimatedSpan delay={2500} className="text-green-500">
<span>✔ Validating Tailwind CSS.</span>
</AnimatedSpan>

<AnimatedSpan delay={3000} className="text-green-500">
<span>✔ Validating import alias.</span>
</AnimatedSpan>

<AnimatedSpan delay={3500} className="text-green-500">
<span>✔ Writing components.json.</span>
</AnimatedSpan>

<AnimatedSpan delay={4000} className="text-green-500">
<span>✔ Checking registry.</span>
</AnimatedSpan>

<AnimatedSpan delay={4500} className="text-green-500">
<span>✔ Updating tailwind.config.ts</span>
</AnimatedSpan>

<AnimatedSpan delay={5000} className="text-green-500">
<span>✔ Updating app/globals.css</span>
</AnimatedSpan>

<AnimatedSpan delay={5500} className="text-green-500">
<span>✔ Installing dependencies.</span>
</AnimatedSpan>

<AnimatedSpan delay={6000} className="text-blue-500">
<span>ℹ Updated 1 file:</span>
<span className="pl-2">- lib/utils.ts</span>
</AnimatedSpan>

<TypingAnimation delay={6500} className="text-muted-foreground">
Success! Project initialization completed.
</TypingAnimation>

<TypingAnimation delay={7000} className="text-muted-foreground">
You may now add components.
</TypingAnimation>
</Terminal>
);
}
119 changes: 119 additions & 0 deletions registry/default/magicui/terminal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { cn } from "@/lib/utils";
import { motion, MotionProps } from "motion/react";
import { useEffect, useRef, useState } from "react";

interface AnimatedSpanProps extends MotionProps {
children: React.ReactNode;
delay?: number;
className?: string;
}

export const AnimatedSpan = ({
children,
delay = 0,
className,
...props
}: AnimatedSpanProps) => (
<motion.div
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: delay / 1000 }}
className={cn("grid text-sm font-normal tracking-tight", className)}
{...props}
>
{children}
</motion.div>
);

interface TypingAnimationProps extends MotionProps {
children: string;
className?: string;
duration?: number;
delay?: number;
as?: React.ElementType;
}

export const TypingAnimation = ({
children,
className,
duration = 60,
delay = 0,
as: Component = "span",
...props
}: TypingAnimationProps) => {
if (typeof children !== "string") {
throw new Error("TypingAnimation: children must be a string. Received:");
}

const MotionComponent = motion.create(Component, {
forwardMotionProps: true,
});

const [displayedText, setDisplayedText] = useState<string>("");
const [started, setStarted] = useState(false);
const elementRef = useRef<HTMLElement | null>(null);

useEffect(() => {
const startTimeout = setTimeout(() => {
setStarted(true);
}, delay);
return () => clearTimeout(startTimeout);
}, [delay]);

useEffect(() => {
if (!started) return;

let i = 0;
const typingEffect = setInterval(() => {
if (i < children.length) {
setDisplayedText(children.substring(0, i + 1));
i++;
} else {
clearInterval(typingEffect);
}
}, duration);

return () => {
clearInterval(typingEffect);
};
}, [children, duration, started]);

return (
<MotionComponent
ref={elementRef}
className={cn("text-sm font-normal tracking-tight", className)}
{...props}
>
{displayedText}
</MotionComponent>
);
};

interface TerminalProps {
children: React.ReactNode;
className?: string;
}

export const Terminal = ({ children, className }: TerminalProps) => {
return (
<div
className={cn(
"z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border border-border bg-background",
className,
)}
>
<div className="flex flex-col gap-y-2 border-b border-border p-4">
<div className="flex flex-row gap-x-2">
<div className="h-2 w-2 rounded-full bg-red-500"></div>
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
<div className="h-2 w-2 rounded-full bg-green-500"></div>
</div>
</div>
<pre className="p-4">
<code className="grid gap-y-1 overflow-auto">{children}</code>
</pre>
</div>
);
};
6 changes: 6 additions & 0 deletions registry/registry-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export const examples: Registry = [
registryDependencies: ["aurora-text"],
files: ["example/aurora-text-demo.tsx"],
},
{
name: "terminal-demo",
type: "registry:example",
registryDependencies: ["terminal"],
files: ["example/terminal-demo.tsx"],
},
{
name: "morphing-text-demo",
type: "registry:example",
Expand Down
6 changes: 6 additions & 0 deletions registry/registry-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export const ui: Registry = [
dependencies: ["motion"],
files: ["magicui/line-shadow-text.tsx"],
},
{
name: "terminal",
type: "registry:ui",
dependencies: ["motion"],
files: ["magicui/terminal.tsx"],
},
{
name: "aurora-text",
type: "registry:ui",
Expand Down