From ba1755675006b7508022528f034716a39cdddda8 Mon Sep 17 00:00:00 2001 From: Arghya Das Date: Wed, 29 Jan 2025 23:45:22 +0530 Subject: [PATCH] feat: adding terminal component (#515) * 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 --- __registry__/index.tsx | 26 +++++ config/docs.ts | 6 ++ content/docs/components/terminal.mdx | 66 ++++++++++++ public/r/index.json | 13 +++ public/r/styles/default/terminal.json | 15 +++ registry/default/example/terminal-demo.tsx | 62 +++++++++++ registry/default/magicui/terminal.tsx | 119 +++++++++++++++++++++ registry/registry-examples.ts | 6 ++ registry/registry-ui.ts | 6 ++ 9 files changed, 319 insertions(+) create mode 100644 content/docs/components/terminal.mdx create mode 100644 public/r/styles/default/terminal.json create mode 100644 registry/default/example/terminal-demo.tsx create mode 100644 registry/default/magicui/terminal.tsx diff --git a/__registry__/index.tsx b/__registry__/index.tsx index 9681d5392..0bdcc223d 100644 --- a/__registry__/index.tsx +++ b/__registry__/index.tsx @@ -57,6 +57,19 @@ export const Index: Record = { 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", @@ -880,6 +893,19 @@ export const Index: Record = { 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", diff --git a/config/docs.ts b/config/docs.ts index 0e278803d..0fc9dbb9d 100644 --- a/config/docs.ts +++ b/config/docs.ts @@ -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`, diff --git a/content/docs/components/terminal.mdx b/content/docs/components/terminal.mdx new file mode 100644 index 000000000..f3bba80a1 --- /dev/null +++ b/content/docs/components/terminal.mdx @@ -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 +--- + + + +## Installation + + + + + CLI + Manual + + + +```bash +npx shadcn@latest add "https://magicui.design/r/terminal" +``` + + + + + + + +Copy and paste the following code into your project. + + + + + + + + + +## 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. | diff --git a/public/r/index.json b/public/r/index.json index c49f9b470..8d098da0f 100644 --- a/public/r/index.json +++ b/public/r/index.json @@ -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", diff --git a/public/r/styles/default/terminal.json b/public/r/styles/default/terminal.json new file mode 100644 index 000000000..6ce2f31ee --- /dev/null +++ b/public/r/styles/default/terminal.json @@ -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 \n {children}\n \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(\"\");\n const [started, setStarted] = useState(false);\n const elementRef = useRef(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 \n {displayedText}\n \n );\n};\n\ninterface TerminalProps {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const Terminal = ({ children, className }: TerminalProps) => {\n return (\n \n
\n
\n
\n
\n
\n
\n
\n
\n        {children}\n      
\n \n );\n};\n", + "type": "registry:ui", + "target": "" + } + ] +} \ No newline at end of file diff --git a/registry/default/example/terminal-demo.tsx b/registry/default/example/terminal-demo.tsx new file mode 100644 index 000000000..9f192db27 --- /dev/null +++ b/registry/default/example/terminal-demo.tsx @@ -0,0 +1,62 @@ +import { + AnimatedSpan, + Terminal, + TypingAnimation, +} from "@/registry/default/magicui/terminal"; + +export default function TerminalDemo() { + return ( + + > pnpm dlx shadcn@latest init + + + ✔ Preflight checks. + + + + ✔ Verifying framework. Found Next.js. + + + + ✔ Validating Tailwind CSS. + + + + ✔ Validating import alias. + + + + ✔ Writing components.json. + + + + ✔ Checking registry. + + + + ✔ Updating tailwind.config.ts + + + + ✔ Updating app/globals.css + + + + ✔ Installing dependencies. + + + + ℹ Updated 1 file: + - lib/utils.ts + + + + Success! Project initialization completed. + + + + You may now add components. + + + ); +} diff --git a/registry/default/magicui/terminal.tsx b/registry/default/magicui/terminal.tsx new file mode 100644 index 000000000..a32083db6 --- /dev/null +++ b/registry/default/magicui/terminal.tsx @@ -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) => ( + + {children} + +); + +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(""); + const [started, setStarted] = useState(false); + const elementRef = useRef(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 ( + + {displayedText} + + ); +}; + +interface TerminalProps { + children: React.ReactNode; + className?: string; +} + +export const Terminal = ({ children, className }: TerminalProps) => { + return ( +
+
+
+
+
+
+
+
+
+        {children}
+      
+
+ ); +}; diff --git a/registry/registry-examples.ts b/registry/registry-examples.ts index 01051f9e2..b8b859c22 100644 --- a/registry/registry-examples.ts +++ b/registry/registry-examples.ts @@ -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", diff --git a/registry/registry-ui.ts b/registry/registry-ui.ts index b337cd8fc..24cc99c91 100644 --- a/registry/registry-ui.ts +++ b/registry/registry-ui.ts @@ -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",