Skip to content

Commit

Permalink
feat: adding terminal component (#515)
Browse files Browse the repository at this point in the history
* feat: adding terminal component

* feat: added shadcn primitive approach for improved composability

* fix: rebase

* fix: rebase fix

* fix: update component

---------

Co-authored-by: Dillion Verma <[email protected]>
  • Loading branch information
itsarghyadas and dillionverma authored Jan 29, 2025
1 parent dea9b82 commit ba17556
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 0 deletions.
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

0 comments on commit ba17556

Please sign in to comment.