# Complete documentation of Inspira UI

URL: https://inspira-ui.com/getting-started/Introduction

---
title: Introduction
description: Reusable components built with shadcn-vue, motion-v, and TailwindCSS
icon: "lucide:info"
---

Welcome to [**Inspira UI**](https://inspira-ui.com), a community-driven project for [Vue](https://vuejs.org)!

This collection offers beautifully designed, reusable components, taking inspiration from the amazing work done on both [Aceternity UI](https://ui.aceternity.com) and [Magic UI](https://magicui.design). While we're not officially affiliated with these projects, we have received permission from Aceternity UI's creator to adapt those fantastic designs for the Vue ecosystem. Additionally, Inspira UI includes custom components developed by us and contributed by the community.

### About Inspira UI

Inspira UI is **not** a traditional component library. Instead, it's a curated collection of elegant components you can easily integrate into your applications. Simply pick what you need, copy the code, and customize it to fit your project. The code is yours to use and modify as you like!

### Why Inspira UI?

This project began to fill a gap in the Vue community for a similar set of components. Inspira UI brings the beauty and functionality of Aceternity UI, Magic UI, and custom contributions to Vue, making it easier for developers to build stunning applications.

### Key Features

- Completely [free and open source](https://github.com/unovue/inspira-ui)
- Highly [configurable](/components) to meet your design needs
- A wide range of [components](/components) to choose from
- Optimized for mobile use
- Fully compatible with Nuxt

### Acknowledgments

Special thanks to:

- [Aceternity UI](https://ui.aceternity.com) for inspiring this Vue adaptation.
- [Magic UI](https://magicui.design) for their design inspiration.
- [shadcn-vue](https://www.shadcn-vue.com/) for the Vue port of shadcn-ui and contributing some components for docs.
- [shadcn-docs-nuxt](https://github.com/ZTL-UwU/shadcn-docs-nuxt) for the beautifully crafted Nuxt documentation site.

### About Me

Hi, I'm [Rahul Vashishtha](https://rahulv.dev). I started Inspira UI to bring a similar experience to the Vue ecosystem, inspired by Aceternity UI, Magic UI, and community contributions. I'm continuously working on it to make it better. Feel free to check out my work on [GitHub](https://github.com/rahul-vashishtha) and join me on this journey [here](https://github.com/unovue/inspira-ui)!

Feel free to explore and enjoy building with Inspira UI!

URL: https://inspira-ui.com/getting-started/installation

---
title: Installation
description: How to install Inspira UI in your app.
icon: "lucide:play"
---

This guide will help you install and set up Inspira UI components in your Vue or Nuxt application.

## Getting Started with Inspira UI

::alert{type="info"}
**Note:** If you're using `shadcn-vue`, you can skip step `1` & `4`.
::

::steps

### Set up `tailwindcss`

::alert{type="info"}
Currently Inspira UI supports TailwindCSS v3. Make sure you install TailwindCSS v3.
::

To begin, install `tailwindcss` using [this guide](https://v3.tailwindcss.com/docs/installation).

### Add dependencies

Install libraries for tailwindcss and utilities.

::code-group

```bash [npm]
npm install -D @inspira-ui/plugins clsx tailwind-merge class-variance-authority tailwindcss-animate
```

```bash [pnpm]
pnpm install -D @inspira-ui/plugins clsx tailwind-merge class-variance-authority tailwindcss-animate
```

```bash [bun]
bun add -d @inspira-ui/plugins clsx tailwind-merge class-variance-authority tailwindcss-animate
```

```bash [yarn]
yarn add --dev @inspira-ui/plugins clsx tailwind-merge class-variance-authority tailwindcss-animate
```

::

Install VueUse and other supporting libraries.

::code-group

```bash [npm]
npm install @vueuse/core motion-v
```

```bash [pnpm]
pnpm install @vueuse/core motion-v
```

```bash [bun]
bun add @vueuse/core motion-v
```

```bash [yarn]
yarn add @vueuse/core motion-v
```

::

Follow this guide to setup `motion-v` on [Vue](https://motion.unovue.com/getting-started/installation) and [Nuxt](https://motion.unovue.com/getting-started/installation)

### Update `tailwind.config.js` and `tailwind.css`

Add the following code to your `tailwind.config.js` and your `css` file:

::code-group

```ts [tailwind.config.js]
import animate from "tailwindcss-animate";
import { setupInspiraUI } from "@inspira-ui/plugins";

export default {
  darkMode: "selector",
  safelist: ["dark"],
  prefix: "",
  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        xl: "calc(var(--radius) + 4px)",
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },

  plugins: [animate, setupInspiraUI],
};
```

```css [tailwind.css]
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;

    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;

    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;

    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;

    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;

    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;

    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;

    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;

    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 221.2 83.2% 53.3%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;

    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;

    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;

    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;

    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;

    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;

    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;

    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;

    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 224.3 76.3% 48%;
  }
}
```

::

### Setup `cn` utility

Add following utility to `lib/utils.ts`

```ts [utils.ts]
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export type ObjectValues<T> = T[keyof T];
```

::alert{type="success" icon="lucide:circle-check"}
Great job! Your project is now configured to use Inspira UI.
::

### Optional: Add Icon Support

A variety of Inspira UI components and demos utilize the `<Icon>` component with Iconify icons. Although optional, we recommend installing it for an optimal experience.

To add icon support to your Vue.js or Nuxt.js project, please follow the [Iconify Vue guide](https://iconify.design/docs/icon-components/vue/).

### Start Using Inspira UI 🚀

Now, you can start using Inspira UI components in your project. Choose the components you need, copy the code, and integrate them into your application.

:read-more{title="List of all components" to="/components"}
::

URL: https://inspira-ui.com/getting-started/contribution

---
title: How To Contribute
description: Follow the following guidelines to ensure the smooth collaboration
icon: "lucide:scroll-text"
---

Thank you for your interest in contributing to the **Inspira UI** project! Your contributions help make this project better for everyone. Please take a moment to read through these guidelines to ensure a smooth collaboration.

## Table of Contents

1. [Getting Started](#getting-started)
2. [Code of Conduct](#code-of-conduct)
3. [How to Contribute](#how-to-contribute)
   - [Reporting Bugs](#reporting-bugs)
   - [Suggesting Enhancements](#suggesting-enhancements)
   - [Adding New Components](#adding-new-components)
4. [Project Structure](#project-structure)
5. [Style Guidelines](#style-guidelines)
   - [Coding Standards](#coding-standards)
   - [Component Format](#component-format)
   - [Commit Messages](#commit-messages)
6. [Documentation Guidelines](#documentation-guidelines)
   - [Single-File Components](#single-file-components)
   - [Multi-File Components](#multi-file-components)
7. [Testing](#testing)
8. [License](#license)

---

## Getting Started

- **Fork the Repository**: Create a personal fork of the project on GitHub.
- **Clone Your Fork**: Clone your forked repository to your local machine.
- **Create a Branch**: Create a new branch for your contribution (`git checkout -b feature/YourFeatureName`).
- **Install Dependencies**: Run `pnpm install` to install all necessary dependencies.

## Code of Conduct

By participating in this project, you agree to abide by the [Code of Conduct](./4.code-of-conduct.md), which aims to foster an open and welcoming environment.

## How to Contribute

### Reporting Bugs

If you find a bug, please open an [issue](https://github.com/unovue/inspira-ui/issues){:target="\_blank"} and include:

- A clear and descriptive title.
- Steps to reproduce the issue.
- Expected and actual results.
- Screenshots or code snippets, if applicable.

### Suggesting Enhancements

We welcome suggestions for new features or improvements. Please open an [issue](https://github.com/unovue/inspira-ui/issues){:target="\_blank"} and include:

- A clear and descriptive title.
- A detailed description of the enhancement.
- Any relevant examples or mockups.

### Adding New Components

We appreciate contributions that add new components to the library. Please ensure that:

- The component is generally useful and aligns with the project's goals.
- The component is compatible with both **Nuxt** and **Vue**.
- You follow the coding and documentation guidelines outlined below.
- You include unit tests for your component.

#### Components guidelines

1. **Create or Update `index.ts`**  
   Each folder under `components/content/inspira/ui/<component-folder-name>/` should have an `index.ts` that exports all Vue files. For example:

   ```ts
   // index.ts
   export { default as Book } from "./Book.vue";
   export { default as BookHeader } from "./BookHeader.vue";
   export { default as BookTitle } from "./BookTitle.vue";
   export { default as BookDescription } from "./BookDescription.vue";
   ```

2. **Registry Dependencies:**
   If your new component depends on (or uses) other Inspira UI components, you must update the `COMPONENT_DEPENDENCIES` map in `~/scripts/crawl-content.ts` to reflect those relationships. This helps the library understand which components should be installed together when a user adds them via the CLI.

3. **Nuxt-Only Features:**
   If your new component or its example uses Nuxt-only features such as `<ClientOnly>`, please mention this in the documentation. This ensures users know there may be limitations or additional steps when using the component outside of Nuxt.

   This ensures that users can install the component through the CLI and that all dependencies are properly declared.

4. **Avoid External Components:**
   When creating components, avoid using external UI components (like `<UiButton>` or similar) that are not part of the core Vue.js ecosystem.

5. **Explicit Imports:**
   Even if you're using Nuxt's auto-imports feature during development, always include explicit imports in your component code. This ensures compatibility with Vue.js users who don't have auto-imports. For example:

   ```vue
   <script setup lang="ts">
   import { ref, onMounted } from "vue";
   import { useWindowSize } from "@vueuse/core";
   // Include all imports explicitly
   </script>
   ```

6. **Icon Usage:**
   If you need icons in your demos or components, use the built-in `<Icon>` component rather than pasting raw SVGs into your templates.

## Project Structure

Understanding the project structure is crucial for effective contribution:

- **Components Directory**:
  - Main components should be placed in `components/content/inspira/ui/<component-folder-name>/`.
    - Include an `index.ts` file to export each component within that folder.
  - Example components should be placed in `components/content/inspira/examples/<component-folder-name>/`.
- **Documentation**:
  - Documentation files are located in the `content/2.components/<category>/` directory.
  - After adding a component, write its documentation in this directory.

## Style Guidelines

### Coding Standards

- **Language**: All components should be written in **Vue.js** with TypeScript support.
- **Styling**: Use **Tailwind CSS** classes for styling whenever possible.
- **Naming Conventions**: Use `PascalCase` for component names and filenames.

### Component Format

Your Vue components should adhere to the following structure:

```vue
<!-- Template -->
<template>
  <!-- Your template code goes here -->
</template>

<!-- Script (if required) -->
<script lang="ts" setup>
// Your script code goes here
</script>

<!-- Styles (if required) -->
<style scoped>
/* Your styles go here */
</style>
```

**Props typing and code style**

Refer to this Vue.js documentation page -> [https://vuejs.org/api/sfc-script-setup#type-only-props-emit-declarations](https://vuejs.org/api/sfc-script-setup#type-only-props-emit-declarations)

```vue
<script lang="ts" setup>
// DON'T ⛔️
const props = defineProps({
  whatever: { type: String, required: true },
  optional: { type: String, default: "default" },
});

// DO ✅
interface Props {
  whatever: string;
  optional?: string;
}

const props = withDefaults(defineProps<Props>(), { optional: "default" });

// Or DO ✅ Props destructure (v3.5+)
interface Props {
  msg?: string;
  labels?: string[];
}

const { msg = "hello", labels = ["one", "two"] } = defineProps<Props>();
</script>
```

**Constants, interfaces, types and variants**

For reusability purposes, you can also add an `index.ts` file at the root of the component folder to export interfaces, constants, and other useful code elements. Keep in mind that developers will copy and paste the component code into their projects, so it should be very easy to customize according to their standards.

Contants have to be `CAPS_CAMEL_CASE` in order to identify them clearly inside the code. And `prefix` them.
Please never use Enums; use `{} as const` instead. 😘

```typescript
// DON'T ⛔️
const Direction = { Top: 'top'} as const
const ComponentNameDirection = { Top: 'top'} as const

// DON'T ⛔️
enum COMPONENT_NAME_DIRECTION_WRONG = { Top = 'top'};

// DO ✅
import type { ObjectValues } from "@/lib/utils";
export const COMPONENT_NAME_DIRECTION = { Top: 'top', Bottom: 'bottom'} as const

//Types and Interfaces should use CamelCase to differentiate them from constants and variables.
export type ComponentNameDirection = ObjectValues<typeof COMPONENT_NAME_DIRECTION>;

interface {
   direction: ComponentNameDirection; //Enforce correct value : 'top' or 'bottom'
}
```

You can check the `PatternBackground` component files `components/content/inspira/ui/pattern-background` for a complete example.

**Notes:**

- Use `<script lang="ts" setup>` for TypeScript and the Composition API.
- Keep styles scoped to prevent conflicts.
- Ensure compatibility with both **Nuxt** and **Vue**.

### Commit Messages

- Use the [Conventional Commits](https://www.conventionalcommits.org/) format.
- Begin with a type (`feat`, `fix`, `docs`, etc.) followed by a short description.
- Example: `feat: add TextHoverEffect component`

## Documentation Guidelines

Proper documentation is crucial for users to understand and effectively use the components. Follow these guidelines when adding documentation for new components.

### Steps to Add a New Component

1. **Create the Component**

   - Place the main component in `components/content/inspira/ui/<component-folder-name>/`.
   - Follow the [Component Format](#component-format) specified above.
   - If the component requires examples or demos, add demo components to `components/content/inspira/examples/<component-folder-name>/`.

2. **Write Documentation**

   - Add a new Markdown file in `content/2.components/<category>/` for your component's documentation.
   - Use the appropriate template based on whether your component is single-file or multi-file (see below).
   - Add utility classes section if applicable to your component.
   - Mention the **Credits** and source if the component is ported from any other UI library or taken inspiration from any other source.

3. **Ensure Compatibility**

   - Test your component in both **Nuxt** and **Vue** environments.

### Single-File Components

For components that are contained within a single `.vue` file, use the following template:

1. **Front Matter**

   Begin with YAML front matter that includes the `title` and `description`:

   ```yaml
   ---
   title: Your Component Title
   description: A brief description of what your component does.
   ---
   ```

2. **Preview Section**

   Use the `ComponentLoader` to display a live preview of the component. The `id` should be set to the folder name of your component in `components/content/inspira/examples/`. In case, there is no folder, then `id` is not required.

   ```markdown
   ```vue
// Component source not found for YourComponentDemo.vue
```
   ```

3. **Alerts**

   If your component has special requirements or dependencies, add an alert section before the installation instructions:

   ```markdown
   ::alert{type="info"}
   **Note:** This component requires `package-name` as a dependency.
   ::

   ::alert{type="warning"}
   **Note:** This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
   ::
   ```

4. **Installation**

   Include both CLI and manual installation instructions. If additional setup is required (e.g., dependencies, Tailwind config updates), use a stepper to list all needed steps.

   ```markdown
   ## Install using CLI

   ```vue
<InstallationCli component-id="your-component-folder-name" />
```

   ## Install Manually

   Copy and paste the following code

   ```vue
// Component source not found for YourComponent.vue
```
   ```

5. **API Documentation**

   Provide a table listing all props:

   ```markdown
   ## API

   | Prop Name | Type      | Default | Description                    |
   | --------- | --------- | ------- | ------------------------------ |
   | `prop1`   | `string`  | `''`    | Description of prop1.          |
   | `prop2`   | `number`  | `0`     | Description of prop2.          |
   | `prop2`   | `?number` | `0`     | Description of prop2 optional. |
   ```

**Example:**

```markdown
---
title: Text Hover Effect
description: A text hover effect that animates and outlines gradient on hover, as seen on x.ai
---

```vue
<template>
  <ClientOnly>
    <div class="flex h-auto items-center justify-center max-lg:w-full min-md:flex-1">
      <TextHoverEffect
        class="w-[90%] min-lg:min-h-64"
        text="INSPIRA"
      />
    </div>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="text-hover-effect" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <svg
    ref="svgRef"
    width="100%"
    height="100%"
    viewBox="0 0 300 100"
    xmlns="http://www.w3.org/2000/svg"
    class="select-none"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
    @mousemove="handleMouseMove"
    @touchstart="handleTouchStart"
    @touchmove="handleTouchMove"
    @touchend="handleTouchEnd"
  >
    <defs>
      <linearGradient
        id="textGradient"
        gradientUnits="userSpaceOnUse"
        cx="50%"
        cy="50%"
        r="25%"
      >
        <stop
          v-if="hovered"
          offset="0%"
          stop-color="var(--yellow-500)"
        />
        <stop
          v-if="hovered"
          offset="25%"
          stop-color="var(--red-500)"
        />
        <stop
          v-if="hovered"
          offset="50%"
          stop-color="var(--blue-500)"
        />
        <stop
          v-if="hovered"
          offset="75%"
          stop-color="var(--cyan-500)"
        />
        <stop
          v-if="hovered"
          offset="100%"
          stop-color="var(--violet-500)"
        />
      </linearGradient>

      <!-- Radial Gradient -->
      <radialGradient
        id="revealMask"
        gradientUnits="userSpaceOnUse"
        r="20%"
        :cx="maskPosition.cx"
        :cy="maskPosition.cy"
        :style="{
          transition: `cx ${transitionDuration}ms ease-out, cy ${transitionDuration}ms ease-out`,
        }"
      >
        <stop
          offset="0%"
          stop-color="white"
        />
        <stop
          offset="100%"
          stop-color="black"
        />
      </radialGradient>

      <mask id="textMask">
        <rect
          x="0"
          y="0"
          width="100%"
          height="100%"
          fill="url(#revealMask)"
        />
      </mask>
    </defs>

    <text
      x="50%"
      y="50%"
      text-anchor="middle"
      dominant-baseline="middle"
      :stroke-width="strokeWidth"
      :style="{ opacity: hovered ? opacity : 0 }"
      class="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
    >
      {{ text }}
    </text>

    <!-- Animated Text Stroke -->
    <text
      x="50%"
      y="50%"
      text-anchor="middle"
      dominant-baseline="middle"
      :stroke-width="strokeWidth"
      :style="strokeStyle"
      class="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
    >
      {{ text }}
    </text>

    <text
      x="50%"
      y="50%"
      text-anchor="middle"
      dominant-baseline="middle"
      stroke="url(#textGradient)"
      :stroke-width="strokeWidth"
      mask="url(#textMask)"
      class="fill-transparent font-[helvetica] text-7xl font-bold"
    >
      {{ text }}
    </text>
  </svg>
</template>

<script setup lang="ts">
import { ref, reactive, computed } from "vue";

interface Props {
  strokeWidth?: number;
  text: string;
  duration?: number;
  opacity?: number;
}

const props = withDefaults(defineProps<Props>(), {
  strokeWidth: 0.75,
  duration: 200,
  opacity: 0.75,
});

const svgRef = ref<SVGSVGElement | null>(null);
const cursor = reactive({ x: 0, y: 0 });
const hovered = ref(false);

// Set transition duration for smoother animation
const transitionDuration = props.duration ? props.duration * 1000 : 200;

// Reactive gradient position
const maskPosition = computed(() => {
  if (svgRef.value) {
    const svgRect = svgRef.value.getBoundingClientRect();
    const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
    const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
    return { cx: `${cxPercentage}%`, cy: `${cyPercentage}%` };
  }
  return { cx: "50%", cy: "50%" }; // Default position
});

// Reactive style for stroke animation
const strokeStyle = computed(() => ({
  strokeDashoffset: hovered.value ? "0" : "1000",
  strokeDasharray: "1000",
  transition: "stroke-dashoffset 4s ease-in-out, stroke-dasharray 4s ease-in-out",
}));

function handleMouseEnter() {
  hovered.value = true;
}

function handleMouseLeave() {
  hovered.value = false;
}

function handleMouseMove(e: MouseEvent) {
  cursor.x = e.clientX;
  cursor.y = e.clientY;
}

// Touch support
function handleTouchStart(e: TouchEvent) {
  hovered.value = true;
  handleTouchMove(e); // Update the position on touch start
}

function handleTouchMove(e: TouchEvent) {
  const touch = e.touches[0];
  cursor.x = touch.clientX;
  cursor.y = touch.clientY;
}

function handleTouchEnd() {
  hovered.value = false;
}
</script>

<style scoped>
.select-none {
  user-select: none;
}
</style>

```

## API

| Prop Name     | Type     | Default  | Description                                               |
| ------------- | -------- | -------- | --------------------------------------------------------- |
| `text`        | `string` | Required | The text to be displayed with the hover effect.           |
| `duration`    | `number` | `200`    | The duration of the mask transition animation in seconds. |
| `strokeWidth` | `number` | `0.75`   | The width of the text stroke.                             |
| `opacity`     | `number` | `null`   | The opacity of the text.                                  |
```

In this example, the `id` used in both `ComponentLoader`, `CodeViewer` and `InstallationCli` is `text-hover-effect`, which matches the folder name where the component and its demo are stored.

### Multi-File Components

For components that consist of multiple files, such as a main component and several sub-components or variants, use the following template:

1. **Front Matter**

   Begin with YAML front matter:

   ```yaml
   ---
   title: Your Components Group Title
   description: A brief description of what this group of components does.
   ---
   ```

2. **Preview Sections**

   Include multiple `ComponentLoader` sections for each example or variant. The `id` should be set to the folder name of your component in `components/content/inspira/examples/`.

   ```markdown
   ```vue
// Component source not found for ComponentVariantDemo.vue
```
   ```

3. **Alerts**

   If your component has special requirements or dependencies, add an alert section before the installation instructions:

   ```markdown
   ::alert{type="info"}
   **Note:** This component requires `package-name` as a dependency.
   ::

   ::alert{type="warning"}
   **Note:** This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
   ::
   ```

4. **Installation**

   Include both CLI and manual installation instructions. If additional setup is required (e.g., dependencies, Tailwind config updates), use a stepper to list all needed steps.

   ```markdown
   ## Install using CLI

   ```vue
<InstallationCli component-id="your-component-folder-name" />
```

   ## Install Manually

   Copy and paste the following code in the same folder

   ::code-group

   :CodeViewerTab{label="YourComponent.vue" language="vue" componentName="YourComponent" type="ui" id="your-component-folder-name"}
   :CodeViewerTab{filename="YourComponent2.vue" language="vue" componentName="YourComponent2" type="ui" id="your-component-folder-name"}

   ::
   ```

5. **API Documentation**

   Provide comprehensive API documentation covering all components:

   ```markdown
   ## API

   | Prop Name | Type     | Default | Description                             |
   | --------- | -------- | ------- | --------------------------------------- |
   | `prop1`   | `string` | `''`    | Description applicable to all variants. |
   ```

**Example:**

```markdown
---
title: Pattern Background
description: Simple animated pattern background to make your sections stand out.
---

Grid background with dot
```vue
<template>
  <PatternBackground
    :animate="true"
    :direction="PATTERN_BACKGROUND_DIRECTION.TopRight"
    :variant="PATTERN_BACKGROUND_VARIANT.Dot"
    class="flex h-[36rem] w-full items-center justify-center"
    :speed="PATTERN_BACKGROUND_SPEED.Slow"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Dot Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_SPEED,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="pattern-background" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="PatternBackground.vue" language="vue" componentName="PatternBackground" type="ui" id="pattern-background"}
:CodeViewerTab{filename="index.ts" language="typescript" componentName="index" type="ui" id="pattern-background" extension="ts"}

::

## Examples

Grid background with big dot and ellipse on top
```vue
<template>
  <PatternBackground
    :animate="true"
    :direction="PATTERN_BACKGROUND_DIRECTION.Bottom"
    :variant="PATTERN_BACKGROUND_VARIANT.BigDot"
    class="flex h-[36rem] w-full items-center justify-center"
    :speed="PATTERN_BACKGROUND_SPEED.Slow"
    :mask="PATTERN_BACKGROUND_MASK.EllipseTop"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Big Dot Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_MASK,
  PATTERN_BACKGROUND_SPEED,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

Grid background without animation
```vue
<template>
  <PatternBackground
    :direction="PATTERN_BACKGROUND_DIRECTION.TopRight"
    :variant="PATTERN_BACKGROUND_VARIANT.Grid"
    class="flex h-[36rem] w-full items-center justify-center"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Grid Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

Small grid background with animation
```vue
<template>
  <PatternBackground
    :animate="true"
    :direction="PATTERN_BACKGROUND_DIRECTION.Right"
    :variant="PATTERN_BACKGROUND_VARIANT.Grid"
    class="flex h-[36rem] w-full items-center justify-center"
    size="xs"
    :speed="PATTERN_BACKGROUND_SPEED.Slow"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Small Grid Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_SPEED,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

## API

| Prop Name   | Type                                                                                                   | Default   | Description                                                                                                                                                    |
| ----------- | ------------------------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `animate`   | `boolean`                                                                                              | `false`   | Set `true` if you want to animate the background.                                                                                                              |
| `direction` | `top` \| `bottom` \| `left` \| `right` \| `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top`     | Direction of the animation movement. You can use the const `PATTERN_BACKGROUND_DIRECTION.`                                                                     |
| `direction` | `grid` \| `dot`                                                                                        | `grid`    | Type of pattern. You can use the const `PATTERN_BACKGROUND_VARIANT.`                                                                                           |
| `size`      | `xs` \| `sm` \| `md` \| `lg`                                                                           | `md`      | Size of the background pattern.                                                                                                                                |
| `mask`      | `ellipse` \| `ellipse-top`                                                                             | `ellipse` | Add a mask over the background pattern. You can use the const `PATTERN_BACKGROUND_MASK.`                                                                       |
| `speed`     | `number`                                                                                               | `10000`   | Duration of the animation in `ms`, the bigger it is, the slower the animation. (`20000` slower than `5000`). You can use the const `PATTERN_BACKGROUND_SPEED.` |

### Custom variants, values and constants

You can customize your needs directly within the `index.ts` file. See code below.

## Credits

- Inspired by [Magic UI's Dot Pattern](https://magicui.design/docs/components/dot-pattern) component.
- Inspired by [Magic UI's Grid Pattern](https://magicui.design/docs/components/grid-pattern) component.
- Inspired by [Magic UI's Animated Grid Pattern](https://magicui.design/docs/components/animated-grid-pattern) component.
- Credits to [Nathan De Pachtere](https://nathandepachtere.com/) for porting this component.
```

## Testing

- **Unit Tests**: Write unit tests for your component if applicable.
- **Cross-Environment Testing**: Ensure that your component works correctly in both **Nuxt** and **Vue** environments.
- **Visual Testing**: Check the component visually to ensure it renders correctly.
- **CLI Installation Testing**: After updating the registry with `pnpm build:registry`, test the component installation in a separate project by referencing the local registry URL. For example:
  ```sh
  npx shadcn-vue@latest add "https://localhost:3000/r/<component-name>"
  ```

## Additional Notes

- **Component Names**: Use `PascalCase` for component filenames and names.
- **IDs**: In `CodeViewer`, `CodeViewerTab`, and `ComponentLoader`, the `id` parameter should be set to the **folder name** where the component is stored in `components/content/inspira/ui/<component-folder-name>/` and `components/content/inspira/examples/<component-folder-name>/`. This helps in correctly linking the code and examples in the documentation.
- **Demo Components**: For each component, create a corresponding `Demo` component used in the `ComponentLoader` for previews, and place it in `components/content/inspira/examples/<component-folder-name>/`.
- **Localization**: If your component supports multiple languages, include details in the documentation.

## License

By contributing, you agree that your contributions will be licensed under the [MIT License](https://github.com/unovue/inspira-ui/blob/main/LICENSE){:target="\_blank"}.

URL: https://inspira-ui.com/getting-started/code-of-conduct

---
title: Code of Conduct
description: Code of Conduct outlines our expectation for participant behavior as well as consequences for unacceptable conduct.
icon: "lucide:award"
---

## Introduction

We are committed to providing a friendly, safe, and welcoming environment for everyone involved in the **Inspira UI** project. This Code of Conduct outlines our expectations for participant behavior as well as the consequences for unacceptable conduct.

## Our Pledge

In the interest of fostering an open and inclusive community, we pledge to make participation in our project and community a harassment-free experience for everyone, regardless of:

- Age
- Body size
- Disability
- Ethnicity
- Gender identity and expression
- Level of experience
- Nationality
- Personal appearance
- Race
- Religion
- Sexual identity and orientation

## Expected Behavior

All participants in our community are expected to:

- **Be Respectful**: Show empathy and kindness towards others.
- **Be Considerate**: Remember that your actions and words affect others.
- **Be Collaborative**: Work together to achieve common goals.
- **Communicate Effectively**: Use clear and constructive language.
- **Demonstrate Professionalism**: Act professionally and take responsibility for your actions.

## Unacceptable Behavior

The following behaviors are considered unacceptable within our community:

- **Harassment and Discrimination**: Including derogatory comments, slurs, or unwanted sexual attention.
- **Abuse and Threats**: Any form of verbal or written abuse, intimidation, or threats.
- **Trolling and Insults**: Provocative or insulting remarks intended to disrupt conversations.
- **Disrespectful Communication**: Including excessive profanity, shouting (using all caps), or interrupting others.
- **Personal Attacks**: Targeting an individual with the intent to harass or belittle.

## Reporting Guidelines

If you experience or witness unacceptable behavior, or have any other concerns, please report it as soon as possible by contacting the project maintainers on our **Discord channel**:

[Inspira UI Discord Channel](https://discord.gg/Xbh5DwJRc9)

When reporting an incident, please include:

- **Your Contact Information**: Your Discord username or any preferred method of contact.
- **Names of Those Involved**: Real names or usernames of the individuals involved.
- **Description of the Incident**: A clear and concise account of what happened.
- **Supporting Evidence**: Any relevant messages, screenshots, or context that can help us understand the situation.

All reports will be handled confidentially.

## Enforcement

Project maintainers are responsible for ensuring compliance with this Code of Conduct and will take appropriate action in response to any behavior that is deemed unacceptable. Actions may include:

- A private warning to the offender.
- Temporary or permanent ban from participation in the project and Discord channel.
- Removal of contributions that violate the Code of Conduct.

## Scope

This Code of Conduct applies to all project spaces, including but not limited to:

- GitHub repositories
- Issue trackers
- Pull requests
- Project-related forums and chat channels
- Social media interactions pertaining to the project
- The official **Inspira UI Discord channel**

It also applies when an individual is representing the project or its community in public spaces.

## Appeal Process

Any individual who is subjected to disciplinary action has the right to appeal the decision by contacting the project maintainers through the **Discord channel** within one week of the action. The appeal will be reviewed, and a final decision will be communicated.

## Privacy

All reports of unacceptable behavior will be handled with discretion. We will respect the privacy of the reporter and the accused.

## Acknowledgments

We thank all contributors and community members for helping to create a positive environment. This Code of Conduct is adapted from best practices and guidelines used in open-source communities.

## Contact Information

For questions or concerns about this Code of Conduct, please contact the project maintainers on our **Discord channel**:

[Inspira UI Discord Channel](https://discord.gg/Xbh5DwJRc9)

URL: https://inspira-ui.com/components/backgrounds/aurora-background

---
title: Aurora Background
description: A subtle Aurora or Southern Lights background for your website.
---

```vue
<template>
  <AuroraBackground>
    <Motion
      as="div"
      :initial="{ opacity: 0, y: 40, filter: 'blur(10px)' }"
      :in-view="{
        opacity: 1,
        y: 0,
        filter: 'blur(0px)',
      }"
      :transition="{
        delay: 0.3,
        duration: 0.8,
        ease: 'easeInOut',
      }"
      class="relative flex flex-col items-center justify-center gap-4 px-4"
    >
      <div class="text-center text-3xl font-bold md:text-7xl dark:text-white">
        Background lights are cool you know.
      </div>
      <div class="py-4 text-base font-extralight md:text-4xl dark:text-neutral-200">
        And this, is chemical burn.
      </div>
      <button
        class="w-fit rounded-full bg-black px-4 py-2 text-white dark:bg-white dark:text-black"
      >
        Burn it now
      </button>
    </Motion>
  </AuroraBackground>
</template>

<script setup lang="ts">
import { Motion } from "motion-v";
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="aurora-background" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <main>
    <div
      v-bind="props"
      :class="
        cn(
          'relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
          props.class,
        )
      "
    >
      <div class="absolute inset-0 overflow-hidden">
        <div
          :class="
            cn(
              'filter blur-[10px] invert dark:invert-0 pointer-events-none absolute -inset-[10px] opacity-50 will-change-transform;',
              '[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]',
              '[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]',
              '[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]',
              '[background-image:var(--white-gradient),var(--aurora)] dark:[background-image:var(--dark-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%]',
              'aurora-background-gradient-after',
              'aurora-gradient-animation',
              props.radialGradient &&
                `[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`,
            )
          "
        ></div>
      </div>
      <slot />
    </div>
  </main>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";

interface AuroraBackgroundProps {
  radialGradient?: boolean;
  class?: string;
}

const props = withDefaults(defineProps<AuroraBackgroundProps>(), {
  radialGradient: true,
});
</script>

<style scoped>
.aurora-background-gradient-after {
  @apply after:content-[""] 
          after:absolute 
          after:inset-0 
          after:[background-image:var(--white-gradient),var(--aurora)]
          after:dark:[background-image:var(--dark-gradient),var(--aurora)]
          after:[background-size:200%,_100%]
          after:[background-attachment:fixed] 
          after:mix-blend-difference;
}

.aurora-gradient-animation::after {
  animation: animate-aurora 60s linear infinite;
}

@keyframes animate-aurora {
  0% {
    background-position:
      50% 50%,
      50% 50%;
  }
  100% {
    background-position:
      350% 50%,
      350% 50%;
  }
}
</style>

```

## API

| Prop Name        | Type      | Default | Description                                                               |
| ---------------- | --------- | ------- | ------------------------------------------------------------------------- |
| `class`          | `string`  | `-`     | Additional CSS classes to apply to the component for styling.             |
| `radialGradient` | `boolean` | `true`  | Determines whether a radial gradient effect is applied to the background. |

## Features

- **Slot Support**: Easily add any content inside the component using the default slot.

## Credits

- Credits to [Aceternity UI](https://ui.aceternity.com/components/aurora-background).
- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for porting this component.

URL: https://inspira-ui.com/components/backgrounds/bubbles-bg

---
title: Bubbles Background
description: An animated background with floating bubbles.
---

```vue
<template>
  <BubblesBg
    class="h-96 w-full"
    :blur="4"
  >
    <div class="flex size-full flex-col items-center justify-center">
      <span class="font-heading text-6xl font-bold text-background opacity-80 backdrop-blur-md">
        Hello
      </span>
    </div>
  </BubblesBg>
</template>

```

::alert{type="info"}
**Note:** This component uses Three.js & requires `three` npm package as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="bubbles-bg" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install three
npm install -D @types/three
```

```bash [pnpm]
pnpm install three
pnpm install -D @types/three
```

```bash [bun]
bun add three
bun add -d @types/three
```

```bash [yarn]
yarn add three
yarn add --dev @types/three
```

::

Copy and paste the following code

```vue
<template>
  <div
    ref="bubbleParentContainer"
    class="relative h-72 w-full overflow-hidden"
  >
    <div ref="bubbleCanvasContainer"></div>
    <div
      :style="{
        '--bubbles-blur': `${blur}px`,
      }"
      class="absolute inset-0 z-[2] size-full backdrop-blur-[--bubbles-blur]"
    >
      <slot />
    </div>
  </div>
</template>

<script setup lang="ts">
import {
  ShaderMaterial,
  SphereGeometry,
  Vector3,
  Color,
  MathUtils,
  Mesh,
  Clock,
  WebGLRenderer,
  Scene,
  PerspectiveCamera,
} from "three";
import { ref, onMounted, onBeforeUnmount } from "vue";

defineProps({
  blur: {
    type: Number,
    default: 0,
  },
});

const bubbleParentContainer = ref<HTMLElement | null>(null);
const bubbleCanvasContainer = ref<HTMLElement | null>(null);
let renderer: WebGLRenderer;
let scene: Scene;
let camera: PerspectiveCamera;
let clock: Clock;
const spheres: Mesh[] = [];

const BG_COLOR_BOTTOM_BLUISH = rgb(170, 215, 217);
const BG_COLOR_TOP_BLUISH = rgb(57, 167, 255);
const BG_COLOR_BOTTOM_ORANGISH = rgb(255, 160, 75);
const BG_COLOR_TOP_ORANGISH = rgb(239, 172, 53);

const SPHERE_COLOR_BOTTOM_BLUISH = rgb(120, 235, 124);
const SPHERE_COLOR_TOP_BLUISH = rgb(0, 167, 255);
const SPHERE_COLOR_BOTTOM_ORANGISH = rgb(235, 170, 0);
const SPHERE_COLOR_TOP_ORANGISH = rgb(255, 120, 0);

const SPHERE_COUNT = 250;
const SPHERE_SCALE_COEFF = 3;
const ORBIT_MIN = SPHERE_SCALE_COEFF + 2;
const ORBIT_MAX = ORBIT_MIN + 10;
const RAND_SEED = 898211544;

const rand = seededRandom(RAND_SEED);

const { PI, cos, sin } = Math;
const PI2 = PI * 2;
const sizes = new Array(SPHERE_COUNT).fill(0).map(() => randRange(1) * Math.pow(randRange(), 3));
const orbitRadii = new Array(SPHERE_COUNT)
  .fill(0)
  .map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
const positions: [number, number, number][] = orbitRadii.map((rad, i) => [
  rad * cos(thetas[i]) * sin(phis[i]),
  rad * sin(thetas[i]) * sin(phis[i]),
  rad * cos(phis[i]),
]);

const sphereGeometry = new SphereGeometry(SPHERE_SCALE_COEFF);
const sphereMaterial = getGradientMaterial(
  SPHERE_COLOR_BOTTOM_BLUISH,
  SPHERE_COLOR_TOP_BLUISH,
  SPHERE_COLOR_BOTTOM_ORANGISH,
  SPHERE_COLOR_TOP_ORANGISH,
);

const bgGeometry = new SphereGeometry();
bgGeometry.scale(-1, 1, 1);
const bgMaterial = getGradientMaterial(
  BG_COLOR_BOTTOM_BLUISH,
  BG_COLOR_TOP_BLUISH,
  BG_COLOR_BOTTOM_ORANGISH,
  BG_COLOR_TOP_ORANGISH,
);
bgMaterial.uniforms.uTemperatureVariancePeriod.value = new Vector3(0, 0, 0.1);

function seededRandom(a: number) {
  return function () {
    a |= 0;
    a = (a + 0x9e3779b9) | 0;
    var t = a ^ (a >>> 16);
    t = Math.imul(t, 0x21f0aaad);
    t = t ^ (t >>> 15);
    t = Math.imul(t, 0x735a2d97);
    return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;
  };
}

function randRange(n = 1) {
  return rand() * n;
}

function rgb(r: number, g: number, b: number) {
  return new Color(r / 255, g / 255, b / 255);
}

function getGradientMaterial(
  colorBottomWarm: Color,
  colorTopWarm: Color,
  colorBottomCool: Color,
  colorTopCool: Color,
) {
  return new ShaderMaterial({
    uniforms: {
      colorBottomWarm: {
        value: new Color().copy(colorBottomWarm),
      },
      colorTopWarm: {
        value: new Color().copy(colorTopWarm),
      },
      colorBottomCool: {
        value: new Color().copy(colorBottomCool),
      },
      colorTopCool: {
        value: new Color().copy(colorTopCool),
      },
      uTemperature: {
        value: 0.0,
      },
      uTemperatureVariancePeriod: {
        value: new Vector3(0.08, 0.1, 0.2),
      },
      uElapsedTime: {
        value: 0,
      },
    },
    vertexShader: `
      uniform vec4 uTemperatureVariancePeriod;
      uniform float uTemperature;
      uniform float uElapsedTime;
      varying float topBottomMix;
      varying float warmCoolMix;

      void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
        topBottomMix = normal.y;
        warmCoolMix = 0.6 * uTemperature +
          0.4 * (sin(
          (uElapsedTime + gl_Position.x) * uTemperatureVariancePeriod.x +
          (uElapsedTime + gl_Position.y) * uTemperatureVariancePeriod.y +
          (uElapsedTime + gl_Position.z) * uTemperatureVariancePeriod.z) * 0.5 + 0.5);
      }
    `,
    fragmentShader: `
      uniform vec3 colorBottomWarm;
      uniform vec3 colorTopWarm;
      uniform vec3 colorBottomCool;
      uniform vec3 colorTopCool;

      varying float topBottomMix;
      varying float warmCoolMix;

      void main() {
        gl_FragColor = vec4(mix(
          mix(colorTopCool, colorTopWarm, warmCoolMix),
          mix(colorBottomCool, colorBottomWarm, warmCoolMix),
          topBottomMix), 1.0);
      }
    `,
  });
}

function createScene() {
  const width = bubbleCanvasContainer.value?.clientWidth || 1;
  const height = bubbleCanvasContainer.value?.clientHeight || 1;
  // Set up the scene, camera, and renderer
  scene = new Scene();
  camera = new PerspectiveCamera(50, width / height, 1, 2000);
  camera.position.x = 0;
  camera.position.y = 0;
  camera.position.z = 23;

  renderer = new WebGLRenderer({ antialias: true });
  renderer.setSize(width, height);
  renderer.setClearColor(BG_COLOR_BOTTOM_BLUISH);

  // Add these properties to allow overlap
  sphereMaterial.depthWrite = false;
  sphereMaterial.depthTest = true; // Keep this true for depth sorting

  if (bubbleCanvasContainer.value) {
    bubbleCanvasContainer.value.appendChild(renderer.domElement);
  }

  // Create the background mesh
  const bgMesh = new Mesh(bgGeometry, bgMaterial);
  // Position the background far behind everything
  bgMesh.position.set(0, 0, -1); // Move the background far back

  // Disable depth testing for the background to ensure it's always behind other objects
  bgMesh.material.depthTest = false;
  bgMesh.renderOrder = -1; // Ensure the background is rendered first

  // Calculate the scale to ensure the background covers the full canvas
  const distance = camera.position.z; // Distance from the camera
  const aspect = camera.aspect;
  const frustumHeight = 2 * distance * Math.tan(MathUtils.degToRad(camera.fov) / 2);
  const frustumWidth = frustumHeight * aspect;

  // Scale the background geometry to match the camera's frustum size
  bgMesh.scale.set(
    frustumWidth / bgGeometry.parameters.radius,
    frustumHeight / bgGeometry.parameters.radius,
    1,
  );

  scene.add(bgMesh); // Add the backgrou

  // Create sphere meshes
  const orbitRadii = new Array(SPHERE_COUNT)
    .fill(0)
    .map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
  const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
  const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
  const positions = orbitRadii.map((rad, i) => [
    rad * cos(thetas[i]) * sin(phis[i]),
    rad * sin(thetas[i]) * sin(phis[i]),
    rad * cos(phis[i]),
  ]);

  for (let i = 0; i < SPHERE_COUNT; i++) {
    const sphere = new Mesh(sphereGeometry, sphereMaterial);
    const [x, y, z] = positions[i];
    const scaleVector = sizes[i];
    sphere.scale.set(scaleVector, scaleVector, scaleVector);
    sphere.position.set(x, y, z);
    spheres.push(sphere);
    scene.add(sphere);
  }

  clock = new Clock();
}

function animate() {
  requestAnimationFrame(animate);

  const elapsed = clock.getElapsedTime();
  const temperature = sin(elapsed * 0.5) * 0.5 + 0.5;

  bgMaterial.uniforms.uTemperature.value = temperature;
  bgMaterial.uniforms.uElapsedTime.value = elapsed;

  sphereMaterial.uniforms.uTemperature.value = temperature;
  sphereMaterial.uniforms.uElapsedTime.value = elapsed;

  // Floating effect for spheres
  spheres.forEach((sphere, index) => {
    const basePosition = positions[index];
    const floatFactor = 2; // Adjust this value to control float intensity
    const speed = 0.3; // Adjust this value to control float speed
    const floatY = sin(elapsed * speed + index) * floatFactor;
    sphere.position.y = basePosition[1] + floatY;
  });

  renderer.render(scene, camera);
}

function updateRendererSize() {
  const width = bubbleParentContainer.value?.clientWidth || 1;
  const height = bubbleParentContainer.value?.clientHeight || 1;

  // Update renderer size and aspect ratio
  renderer.setSize(width, height);
  camera.aspect = width / height;
  camera.updateProjectionMatrix();

  // Recalculate background mesh scale
  const distance = camera.position.z;
  const frustumHeight = 2 * distance * Math.tan(MathUtils.degToRad(camera.fov) / 2);
  const frustumWidth = frustumHeight * camera.aspect;

  // Get the background mesh and update its scale
  const bgMesh = scene.children.find(
    (obj) => obj instanceof Mesh && obj.geometry === bgGeometry,
  ) as Mesh;
  if (bgMesh) {
    bgMesh.scale.set(
      frustumWidth / bgGeometry.parameters.radius,
      frustumHeight / bgGeometry.parameters.radius,
      1,
    );
  }
}

onMounted(() => {
  createScene();
  updateRendererSize();
  window.addEventListener("resize", updateRendererSize);
  animate();
});

onBeforeUnmount(() => {
  window.removeEventListener("resize", updateRendererSize); // Cleanup on component unmount
});
</script>

```
::

## Example

Without Blur or overlay

```vue
<template>
  <BubblesBg class="h-96 w-full" />
</template>

```

## API

| Prop Name | Type     | Default | Description                                                     |
| --------- | -------- | ------- | --------------------------------------------------------------- |
| `blur`    | `number` | `0`     | Amount of blur to apply to the background, specified in pixels. |

## Features

- **Animated Bubble Background**: Renders a captivating background with animated, floating bubbles using 3D graphics.

- **Dynamic Color Gradients**: The bubbles and background smoothly transition between colors over time, creating a visually engaging effect.

- **Customizable Blur Effect**: Use the `blur` prop to adjust the blur intensity applied over the background, enhancing depth perception.

- **Slot Support**: Easily overlay content on top of the animated background using the default slot.

- **Responsive Design**: The component adjusts to fit the width and height of its parent container, ensuring compatibility across different screen sizes.

## Credits

- Built with the [Three.js](https://threejs.org/) library for 3D rendering and animations.
- Inspired from [Tresjs Experiment](https://lab.tresjs.org/experiments/overlay).

URL: https://inspira-ui.com/components/backgrounds/falling-stars

---
title: Falling Stars Background
description: A stunning animated starfield background with glowing and sharp trail effects.
---

```vue
<template>
  <div class="relative flex h-96 flex-col items-center justify-center">
    <FallingStarsBg
      class="bg-white dark:bg-black"
      :color="isDark ? '#FFF' : '#555'"
    />
    <div class="z-[1] flex items-center">
      <span class="text-6xl font-bold text-black dark:text-white">Inspira UI</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const isDark = computed(() => useColorMode().value == "dark");
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="bg-falling-stars" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <canvas
    ref="starsCanvas"
    :class="cn('absolute inset-0 w-full h-full', $props.class)"
  ></canvas>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { cn } from "@/lib/utils";

interface Star {
  x: number;
  y: number;
  z: number;
  speed: number;
}

const props = withDefaults(
  defineProps<{
    color?: string;
    count?: number;
    class?: string;
  }>(),
  {
    color: "#FFF",
    count: 200,
  },
);

const starsCanvas = ref<HTMLCanvasElement | null>(null);
let perspective: number = 0;
let stars: Star[] = [];
let ctx: CanvasRenderingContext2D | null = null;

onMounted(() => {
  const canvas = starsCanvas.value;
  if (!canvas) return;

  window.addEventListener("resize", resizeCanvas);
  resizeCanvas(); // Call it initially to set correct size

  perspective = canvas.width / 2;
  stars = [];

  // Initialize stars
  for (let i = 0; i < props.count; i++) {
    stars.push({
      x: (Math.random() - 0.5) * 2 * canvas.width,
      y: (Math.random() - 0.5) * 2 * canvas.height,
      z: Math.random() * canvas.width,
      speed: Math.random() * 5 + 2, // Speed for falling effect
    });
  }

  animate(); // Start animation
});

function hexToRgb() {
  let hex = props.color.replace(/^#/, "");

  // If the hex code is 3 characters, expand it to 6 characters
  if (hex.length === 3) {
    hex = hex
      .split("")
      .map((char) => char + char)
      .join("");
  }

  // Parse the r, g, b values from the hex string
  const bigint = parseInt(hex, 16);
  const r = (bigint >> 16) & 255; // Extract the red component
  const g = (bigint >> 8) & 255; // Extract the green component
  const b = bigint & 255; // Extract the blue component

  // Return the RGB values as a string separated by spaces
  return {
    r,
    g,
    b,
  };
}

// Function to draw a star with a sharp line and blurred trail
function drawStar(star: Star) {
  const canvas = starsCanvas.value;
  if (!canvas) return;

  ctx = canvas.getContext("2d");
  if (!ctx) return;

  const scale = perspective / (perspective + star.z); // 3D perspective scale
  const x2d = canvas.width / 2 + star.x * scale;
  const y2d = canvas.height / 2 + star.y * scale;
  const size = Math.max(scale * 3, 0.5); // Size based on perspective

  // Previous position for a trail effect
  const prevScale = perspective / (perspective + star.z + star.speed * 15); // Longer trail distance
  const xPrev = canvas.width / 2 + star.x * prevScale;
  const yPrev = canvas.height / 2 + star.y * prevScale;

  const rgb = hexToRgb();

  // Draw blurred trail (longer, with low opacity)
  ctx.save(); // Save current context state for restoring later
  ctx.strokeStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.2)`;
  ctx.lineWidth = size * 2.5; // Thicker trail for a blur effect
  ctx.shadowBlur = 35; // Add blur to the trail
  ctx.shadowColor = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.8)`;
  ctx.beginPath();
  ctx.moveTo(x2d, y2d);
  ctx.lineTo(xPrev, yPrev); // Longer trail
  ctx.stroke();
  ctx.restore(); // Restore context state to remove blur from the main line

  // Draw sharp line (no blur)
  ctx.strokeStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.6)`;
  ctx.lineWidth = size; // The line width is the same as the star's size
  ctx.beginPath();
  ctx.moveTo(x2d, y2d);
  ctx.lineTo(xPrev, yPrev); // Sharp trail
  ctx.stroke();

  // Draw the actual star (dot)
  ctx.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`;
  ctx.beginPath();
  ctx.arc(x2d, y2d, size / 4, 0, Math.PI * 2); // Dot with size matching the width
  ctx.fill();
}

// Function to animate the stars
function animate() {
  const canvas = starsCanvas.value;
  if (!canvas) return;

  ctx = canvas.getContext("2d");
  if (!ctx) return;

  ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas for each frame

  stars.forEach((star) => {
    drawStar(star);

    // Move star towards the screen (decrease z)
    star.z -= star.speed;

    // Reset star when it reaches the viewer (z = 0)
    if (star.z <= 0) {
      star.z = canvas.width;
      star.x = (Math.random() - 0.5) * 2 * canvas.width;
      star.y = (Math.random() - 0.5) * 2 * canvas.height;
    }
  });

  requestAnimationFrame(animate); // Continue animation
}

// Set canvas to full screen
function resizeCanvas() {
  const canvas = starsCanvas.value;
  if (!canvas) return;

  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
}
</script>

```

## API

| Prop Name | Type     | Default  | Description                                 |
| --------- | -------- | -------- | ------------------------------------------- |
| `color`   | `string` | `"#FFF"` | Color of the stars in the starfield.        |
| `count`   | `number` | `200`    | Number of stars displayed in the animation. |

## Features

- **Dynamic Starfield**: Creates a 3D starfield effect with stars moving toward the viewer.
- **Glowing and Sharp Trail Effects**: Each star has a sharp line and a blurred, glowing trail.
- **Customizable**: Adjust the `color` of the stars and control the number of stars using the `count` prop.
- **Responsive Design**: Automatically adapts to the size of the canvas, ensuring a full-screen starfield effect.

## Credits

- Inspired by 3D starfield simulations and trail effects in modern canvas animations.
- Credit to [Prodromos Pantos](https://github.com/prpanto) for porting the original component to Vue & Nuxt.

URL: https://inspira-ui.com/components/backgrounds/flickering-grid

---
title: Flickering Grid
description: A flickering grid background made with Canvas, fully customizable using Tailwind CSS.
---

```vue
<template>
  <ClientOnly>
    <div class="relative size-[600px] w-full overflow-hidden rounded-lg border bg-background">
      <FlickeringGrid
        class="relative inset-0 z-0 [mask-image:radial-gradient(450px_circle_at_center,white,transparent)]"
        :square-size="4"
        :grid-gap="6"
        color="#60A5FA"
        :max-opacity="0.5"
        :flicker-chance="0.1"
        :width="800"
        :height="800"
      />
    </div>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="flickering-grid" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="containerRef"
    :class="cn('w-full h-full', props.class)"
  >
    <canvas
      ref="canvasRef"
      class="pointer-events-none"
      :width="canvasSize.width"
      :height="canvasSize.height"
    />
  </div>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { ref, onMounted, onBeforeUnmount, toRefs, computed } from "vue";

interface FlickeringGridProps {
  squareSize?: number;
  gridGap?: number;
  flickerChance?: number;
  color?: string;
  width?: number;
  height?: number;
  class?: string;
  maxOpacity?: number;
}

const props = withDefaults(defineProps<FlickeringGridProps>(), {
  squareSize: 4,
  gridGap: 6,
  flickerChance: 0.3,
  color: "rgb(0, 0, 0)",
  maxOpacity: 0.3,
});

const { squareSize, gridGap, flickerChance, color, maxOpacity, width, height } = toRefs(props);

const containerRef = ref<HTMLDivElement>();
const canvasRef = ref<HTMLCanvasElement>();
const context = ref<CanvasRenderingContext2D>();

const isInView = ref(false);
const canvasSize = ref({ width: 0, height: 0 });

const computedColor = computed(() => {
  if (!context.value) return "rgba(255, 0, 0,";

  const hex = color.value.replace(/^#/, "");
  const bigint = Number.parseInt(hex, 16);
  const r = (bigint >> 16) & 255;
  const g = (bigint >> 8) & 255;
  const b = bigint & 255;
  return `rgba(${r}, ${g}, ${b},`;
});

function setupCanvas(
  canvas: HTMLCanvasElement,
  width: number,
  height: number,
): {
  cols: number;
  rows: number;
  squares: Float32Array;
  dpr: number;
} {
  const dpr = window.devicePixelRatio || 1;
  canvas.width = width * dpr;
  canvas.height = height * dpr;
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;

  const cols = Math.floor(width / (squareSize.value + gridGap.value));
  const rows = Math.floor(height / (squareSize.value + gridGap.value));

  const squares = new Float32Array(cols * rows);
  for (let i = 0; i < squares.length; i++) {
    squares[i] = Math.random() * maxOpacity.value;
  }
  return { cols, rows, squares, dpr };
}

function updateSquares(squares: Float32Array, deltaTime: number) {
  for (let i = 0; i < squares.length; i++) {
    if (Math.random() < flickerChance.value * deltaTime) {
      squares[i] = Math.random() * maxOpacity.value;
    }
  }
}

function drawGrid(
  ctx: CanvasRenderingContext2D,
  width: number,
  height: number,
  cols: number,
  rows: number,
  squares: Float32Array,
  dpr: number,
) {
  ctx.clearRect(0, 0, width, height);
  ctx.fillStyle = "transparent";
  ctx.fillRect(0, 0, width, height);
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      const opacity = squares[i * rows + j];
      ctx.fillStyle = `${computedColor.value}${opacity})`;
      ctx.fillRect(
        i * (squareSize.value + gridGap.value) * dpr,
        j * (squareSize.value + gridGap.value) * dpr,
        squareSize.value * dpr,
        squareSize.value * dpr,
      );
    }
  }
}

const gridParams = ref<ReturnType<typeof setupCanvas>>();

function updateCanvasSize() {
  const newWidth = width.value || containerRef.value!.clientWidth;
  const newHeight = height.value || containerRef.value!.clientHeight;

  canvasSize.value = { width: newWidth, height: newHeight };
  gridParams.value = setupCanvas(canvasRef.value!, newWidth, newHeight);
}

let animationFrameId: number | undefined;
let resizeObserver: ResizeObserver | undefined;
let intersectionObserver: IntersectionObserver | undefined;
let lastTime = 0;

function animate(time: number) {
  if (!isInView.value) return;

  const deltaTime = (time - lastTime) / 1000;
  lastTime = time;

  updateSquares(gridParams.value!.squares, deltaTime);
  drawGrid(
    context.value!,
    canvasRef.value!.width,
    canvasRef.value!.height,
    gridParams.value!.cols,
    gridParams.value!.rows,
    gridParams.value!.squares,
    gridParams.value!.dpr,
  );
  animationFrameId = requestAnimationFrame(animate);
}

onMounted(() => {
  if (!canvasRef.value || !containerRef.value) return;
  context.value = canvasRef.value.getContext("2d")!;
  if (!context.value) return;

  updateCanvasSize();

  resizeObserver = new ResizeObserver(() => {
    updateCanvasSize();
  });
  intersectionObserver = new IntersectionObserver(
    ([entry]) => {
      isInView.value = entry.isIntersecting;
      animationFrameId = requestAnimationFrame(animate);
    },
    { threshold: 0 },
  );

  resizeObserver.observe(containerRef.value);
  intersectionObserver.observe(canvasRef.value);
});

onBeforeUnmount(() => {
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId);
  }
  resizeObserver?.disconnect();
  intersectionObserver?.disconnect();
});
</script>

```

## API

| Prop Name       | Type     | Default        | Description                            |
| --------------- | -------- | -------------- | -------------------------------------- |
| `squareSize`    | `number` | `4`            | Size of each square in the grid.       |
| `gridGap`       | `number` | `6`            | Gap between squares in the grid.       |
| `flickerChance` | `number` | `0.3`          | Probability of a square flickering.    |
| `color`         | `string` | `rgb(0, 0, 0)` | Color of the squares.                  |
| `width`         | `number` | `-`            | Width of the canvas.                   |
| `height`        | `number` | `-`            | Height of the canvas.                  |
| `class`         | `string` | `-`            | Additional CSS classes for the canvas. |
| `maxOpacity`    | `number` | `0.2`          | Maximum opacity of the squares.        |

## Credits

- Credits to [magicui flickering-grid](https://magicui.design/docs/components/flickering-grid) for this component.

URL: https://inspira-ui.com/components/backgrounds/interactive-grid-pattern

---
title: Interactive Grid Pattern
description: A interactive background grid pattern made with SVGs, fully customizable.
---

```vue
<template>
  <div class="relative grid h-[500px] place-content-center overflow-hidden">
    <p
      class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white"
    >
      Interactive Grid Pattern
    </p>

    <InteractiveGridPattern
      :class="[
        '[mask-image:radial-gradient(350px_circle_at_center,white,transparent)]',
        'inset-0 h-[200%] skew-y-12',
      ]"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="interactive-grid-pattern" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <svg
    :width="gridWidth"
    :height="gridHeight"
    :class="svgClass"
  >
    <rect
      v-for="(_, index) in totalSquares"
      :key="index"
      :x="getX(index)"
      :y="getY(index)"
      :width="width"
      :height="height"
      :class="getRectClass(index)"
      @mouseenter="handleMouseEnter(index)"
      @mouseleave="handleMouseLeave"
    />
  </svg>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { ref, computed, type HTMLAttributes } from "vue";

interface InteractiveGridPatternProps {
  className?: HTMLAttributes["class"];
  squaresClassName?: HTMLAttributes["class"];
  width?: number;
  height?: number;
  squares?: [number, number];
}

const props = withDefaults(defineProps<InteractiveGridPatternProps>(), {
  width: 40,
  height: 40,
  squares: () => [24, 24],
});

const horizontal = computed(() => props.squares[0]);
const vertical = computed(() => props.squares[1]);

const totalSquares = computed(() => horizontal.value * vertical.value);

const hoveredSquare = ref<number | null>(null);

const gridWidth = computed(() => props.width * horizontal.value);
const gridHeight = computed(() => props.height * vertical.value);

function getX(index: number) {
  return (index % horizontal.value) * props.width;
}

function getY(index: number) {
  return Math.floor(index / horizontal.value) * props.height;
}

const svgClass = computed(() =>
  cn("absolute inset-0 h-full w-full border border-gray-400/30", props.className),
);

function getRectClass(index: number) {
  return cn(
    "stroke-gray-400/30 transition-all duration-100 ease-in-out [&:not(:hover)]:duration-1000",
    hoveredSquare.value === index ? "fill-gray-300/30" : "fill-transparent",
    props.squaresClassName,
  );
}

function handleMouseEnter(index: number) {
  hoveredSquare.value = index;
}

function handleMouseLeave() {
  hoveredSquare.value = null;
}
</script>

```

## Examples

```vue
<template>
  <div class="relative grid h-[500px] place-content-center">
    <InteractiveGridPattern
      :class="'[mask-image:radial-gradient(350px_circle_at_center,white,transparent)]'"
      :width="20"
      :height="20"
      :squares="[80, 80]"
      squares-class-name="hover:fill-blue-500"
    />
  </div>
</template>

```

## API

#### Props

| Prop Name          | Type               | Default    | Description                                   |
| ------------------ | ------------------ | ---------- | --------------------------------------------- |
| `className`        | `string`           | -          | Additional classes for styling the component. |
| `squaresClassName` | `string`           | -          | Additional classes for styling the squares.   |
| `width`            | `number`           | `40`       | Width of the square in pixels.                |
| `height`           | `number`           | `40`       | Height of the square in pixels.               |
| `squares`          | `[number, number]` | `[24, 24]` | Number of squares in the grid pattern.        |

## Credits

- Inspired by [MagicUI](https://magicui.design/docs/components/interactive-grid-pattern).
- Credits to [kalix127](https://github.com/kalix127) for porting this component.

URL: https://inspira-ui.com/components/backgrounds/lamp-effect

---
title: Lamp Effect
description: A captivating lamp lighting effect with conic gradients, spotlights, and glowing lines for an immersive visual experience.
---

```vue
<template>
  <LampEffect>
    <span class="font-heading text-6xl"> Lamp Effect </span>
  </LampEffect>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="lamp-effect" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    :class="
      cn(
        'relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-950 w-full rounded-md z-0',
        $props.class,
      )
    "
  >
    <div class="relative isolate z-0 flex w-full flex-1 scale-y-125 items-center justify-center">
      <!-- Conic Gradient -->
      <div
        :style="{
          backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
        }"
        class="animate-conic-gradient bg-gradient-conic absolute inset-auto right-1/2 h-56 w-60 overflow-visible from-cyan-500 via-transparent to-transparent text-white opacity-50 [--conic-position:from_70deg_at_center_top]"
      >
        <div
          class="absolute bottom-0 left-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]"
        />
        <div
          class="absolute bottom-0 left-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_right,white,transparent)]"
        />
      </div>

      <div
        :style="{
          backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
        }"
        class="animate-conic-gradient bg-gradient-conic absolute inset-auto left-1/2 h-56 w-60 from-transparent via-transparent to-cyan-500 text-white opacity-50 [--conic-position:from_290deg_at_center_top]"
      >
        <div
          class="absolute bottom-0 right-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_left,white,transparent)]"
        />
        <div
          class="absolute bottom-0 right-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]"
        />
      </div>

      <div
        class="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl"
      ></div>

      <div
        class="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md"
      ></div>

      <div
        class="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl"
      ></div>

      <!-- Spotlight -->
      <div
        class="animate-spotlight absolute inset-auto z-30 h-36 w-32 -translate-y-24 rounded-full bg-cyan-400 blur-2xl"
      ></div>

      <!-- Glowing Line -->
      <div
        class="animate-glowing-line absolute inset-auto z-50 h-0.5 w-60 -translate-y-28 bg-cyan-400"
      ></div>

      <div class="absolute inset-auto z-40 h-44 w-full translate-y-[-12.5rem] bg-slate-950"></div>
    </div>

    <div class="relative z-50 flex -translate-y-80 flex-col items-center px-5">
      <slot />
    </div>
  </div>
</template>

<script lang="ts" setup>
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

interface LampEffectProps {
  delay?: number;
  duration?: number;
  class?: HTMLAttributes["class"];
}

const props = withDefaults(defineProps<LampEffectProps>(), {
  delay: 0.5,
  duration: 0.8,
});

const durationInSeconds = computed(() => `${props.duration}s`);
const delayInSeconds = computed(() => `${props.delay}s`);
</script>

<style scoped>
/* Spotlight Animation */
.animate-spotlight {
  animation: spotlight-anim ease-in-out v-bind(durationInSeconds) forwards;
  animation-delay: v-bind(delayInSeconds);
}

/* Glowing Line Animation */
.animate-glowing-line {
  animation: glowing-line-anim ease-in-out v-bind(durationInSeconds) forwards;
  animation-delay: v-bind(delayInSeconds);
}

/* Conic Gradient Animation */
.animate-conic-gradient {
  animation: conic-gradient-anim ease-in-out v-bind(durationInSeconds) forwards;
  animation-delay: v-bind(delayInSeconds);
}

/* Keyframes for Spotlight */
@keyframes spotlight-anim {
  from {
    width: 8rem;
  }
  to {
    width: 16rem;
  }
}

/* Keyframes for Glowing Line */
@keyframes glowing-line-anim {
  from {
    width: 15rem;
  }
  to {
    width: 30rem;
  }
}

/* Keyframes for Conic Gradient */
@keyframes conic-gradient-anim {
  from {
    opacity: 0.5;
    width: 15rem;
  }
  to {
    opacity: 1;
    width: 30rem;
  }
}
</style>

```

## API

| Prop Name  | Type     | Default | Description                                    |
| ---------- | -------- | ------- | ---------------------------------------------- |
| `delay`    | `number` | `0.5`   | Delay before the animation starts, in seconds. |
| `duration` | `number` | `0.8`   | Duration of the animation, in seconds.         |
| `class`    | `string` | `""`    | Additional CSS classes for custom styling.     |

## Features

- **Conic Gradient Animation**: Creates a smooth expanding conic gradient effect, giving a dynamic light-source appearance.
- **Spotlight Animation**: The spotlight smoothly expands, providing a focused lighting effect.
- **Glowing Line Effect**: A glowing line animates across the center, simulating a light beam or laser.
- **Customizable Timing**: The `delay` and `duration` props allow for precise control of animation timings.
- **Slot-Based Content**: Supports default slot content, making it easy to overlay text or other components.

## Credits

- Ported from [Aceternity UI](https://ui.aceternity.com/components/lamp-effect)

URL: https://inspira-ui.com/components/backgrounds/particle-whirlpool-bg

---
title: Particle Whirlpool
description: An animated background with swirling particles.
---

```vue
<template>
  <ParticleWhirlpoolBg
    :blur="2"
    class="h-96"
  >
    <div
      class="flex size-full flex-col items-center justify-center font-heading text-5xl text-white"
    >
      Cool Background
    </div>
  </ParticleWhirlpoolBg>
</template>

```

::alert{type="info"}
**Note:** This component uses Three.js & requires `three` & `postprocessing` npm package as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="bg-particle-whirlpool" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install three postprocessing
npm install -D @types/three
```

```bash [pnpm]
pnpm install three postprocessing
pnpm install -D @types/three
```

```bash [bun]
bun add three postprocessing
bun add -d @types/three
```

```bash [yarn]
yarn add three postprocessing
yarn add --dev @types/three
```

::

Copy and paste the following code

```vue
<template>
  <div
    ref="whirlpoolCanvasContainerRef"
    :class="cn('relative w-full h-full', $props.class)"
  >
    <canvas
      ref="whirlpoolCanvasRef"
      class="size-full"
    ></canvas>
    <div
      :style="{
        '--bubbles-blur': `${blur}px`,
      }"
      class="absolute inset-0 backdrop-blur-[--bubbles-blur]"
    ></div>

    <div class="absolute inset-0">
      <slot />
    </div>
  </div>
</template>

<script lang="ts" setup>
import {
  Vector3,
  MathUtils,
  AmbientLight,
  BoxGeometry,
  InstancedMesh,
  PointLight,
  WebGLRenderer,
  Scene,
  Object3D,
  Vector2,
  PerspectiveCamera,
  Raycaster,
  Plane,
  MeshBasicMaterial,
  InstancedBufferAttribute,
} from "three";
import { onMounted, onUnmounted, ref } from "vue";

import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { cn } from "@/lib/utils";

const { randFloat: rnd, randFloatSpread: rndFS } = MathUtils;

type Instance = {
  position: Vector3;
  scale: number;
  scaleZ: number;
  velocity: Vector3;
  attraction: number;
  vLimit: number;
};

const props = defineProps({
  particleCount: {
    type: Number,
    default: 2000,
  },
  class: String,
  blur: {
    type: Number,
    default: 0,
  },
});

const instances: Instance[] = [];
const target = new Vector3();
const dummyO = new Object3D();
const dummyV = new Vector3();
const pointer = new Vector2();

const light = new PointLight(0x0060ff, 0.5);
const raycaster = new Raycaster();

let renderer: WebGLRenderer;
let scene: Scene;
let camera: PerspectiveCamera;
let imesh: InstancedMesh;
let controls: OrbitControls;
let effectComposer: EffectComposer;

const whirlpoolCanvasContainerRef = ref<HTMLCanvasElement>();
const whirlpoolCanvasRef = ref<HTMLCanvasElement>();

loadParticleInstances();

function loadParticleInstances() {
  for (let i = 0; i < props.particleCount; i++) {
    instances.push({
      position: new Vector3(rndFS(200), rndFS(200), rndFS(200)),
      scale: rnd(0.2, 1),
      scaleZ: rnd(0.1, 1),
      velocity: new Vector3(rndFS(2), rndFS(2), rndFS(2)),
      attraction: 0.03 + rnd(-0.01, 0.01),
      vLimit: 1.2 + rnd(-0.1, 0.1),
    });
  }
}

function setupScene() {
  if (!whirlpoolCanvasRef.value) {
    throw new Error("Canvas not initialized");
  }

  const width = whirlpoolCanvasRef.value.clientWidth;
  const height = whirlpoolCanvasRef.value.clientHeight;

  // Set Renderer
  renderer = new WebGLRenderer({
    canvas: whirlpoolCanvasRef.value,
    antialias: true,
  });
  renderer.setSize(width, height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.autoClear = false;

  // Camera
  camera = new PerspectiveCamera();
  camera.aspect = width / height;
  camera.position.set(0, 0, 200);
  camera.updateProjectionMatrix();

  const ambientLight = new AmbientLight(0x808080);
  const pointLight1 = new PointLight(0xff6000);

  const pointLight2 = new PointLight(0xff6000, 0.5);
  pointLight2.position.set(100, 0, 0);

  const pointLight3 = new PointLight(0x0000ff, 0.5);
  pointLight3.position.set(-100, 0, 0);

  const boxGeometry = new BoxGeometry(2, 2, 10);
  const standardMaterial = new MeshBasicMaterial({
    transparent: true,
    opacity: 0.9,
  });

  imesh = new InstancedMesh(boxGeometry, standardMaterial, props.particleCount);

  scene = new Scene();
  scene.add(ambientLight);
  scene.add(pointLight1);
  scene.add(light);
  scene.add(pointLight2);
  scene.add(pointLight3);

  scene.add(imesh);

  controls = new OrbitControls(camera, renderer.domElement);

  effectComposer = new EffectComposer(renderer);
  effectComposer.setSize(width, height);
  effectComposer.addPass(new RenderPass(scene, camera));

  const unrealBloomPass = new UnrealBloomPass(new Vector2(width, height), 1, 0, 0);

  effectComposer.addPass(unrealBloomPass);
}

function init() {
  for (let i = 0; i < props.particleCount; i++) {
    const { position, scale, scaleZ } = instances[i];
    dummyO.position.copy(position);
    dummyO.scale.set(scale, scale, scaleZ);
    dummyO.updateMatrix();
    imesh.setMatrixAt(i, dummyO.matrix);
  }

  const colors = new Float32Array(props.particleCount * 3); // Each color is an RGB triplet

  for (let i = 0; i < props.particleCount; i++) {
    // Assign random colors
    colors[i * 3] = rnd(0, 1); // Red component
    colors[i * 3 + 1] = rnd(0, 1); // Green component
    colors[i * 3 + 2] = rnd(0, 1); // Blue component
  }

  imesh.instanceColor = new InstancedBufferAttribute(colors, 3); // Add the colors as an attribute
  imesh.instanceMatrix.needsUpdate = true;
}

function animate() {
  requestAnimationFrame(animate);
  controls.update();

  light.position.copy(target);

  for (let i = 0; i < props.particleCount; i++) {
    const { position, scale, scaleZ, velocity, attraction, vLimit } = instances[i];

    dummyV.copy(target).sub(position).normalize().multiplyScalar(attraction);

    velocity.add(dummyV).clampScalar(-vLimit, vLimit);
    position.add(velocity);

    dummyO.position.copy(position);
    dummyO.scale.set(scale, scale, scaleZ);
    dummyO.lookAt(dummyV.copy(position).add(velocity));
    dummyO.updateMatrix();
    imesh.setMatrixAt(i, dummyO.matrix);
  }
  imesh.instanceMatrix.needsUpdate = true;

  effectComposer.render();
}

onMounted(() => {
  setupScene();
  init();
  animate();

  whirlpoolCanvasContainerRef.value?.addEventListener("mousemove", onPointerMove);
  whirlpoolCanvasContainerRef.value?.addEventListener("touchmove", onPointerMove);
  window.addEventListener("resize", onWindowResize);
});

function onPointerMove(event: MouseEvent | TouchEvent) {
  if (!renderer || !camera) return;

  let clientX: number;
  let clientY: number;

  // Check if it's a touch event
  if (event instanceof TouchEvent) {
    clientX = event.touches[0].clientX;
    clientY = event.touches[0].clientY;
  } else {
    clientX = (event as MouseEvent).clientX;
    clientY = (event as MouseEvent).clientY;
  }

  const rect = whirlpoolCanvasContainerRef.value!.getBoundingClientRect();
  const x = clientX - rect.left;
  const y = clientY - rect.top;

  // Check if the pointer is within the bounds of the canvas
  if (x >= 0 && x <= rect.width && y >= 0 && y <= rect.height) {
    // Pointer is within canvas bounds
    pointer.x = (x / rect.width) * 2 - 1;
    pointer.y = -(y / rect.height) * 2 + 1;

    // Update the target position in 3D space
    raycaster.setFromCamera(pointer, camera);
    const planeZ = new Plane(new Vector3(0, 0, 1), 0);
    const point = new Vector3();
    raycaster.ray.intersectPlane(planeZ, point);
    target.copy(point);
  } else {
    // Pointer is outside canvas bounds
    // Set target to center of canvas in 3D space
    target.set(0, 0, 0); // Or any default position you prefer
  }
}

function onWindowResize() {
  if (!whirlpoolCanvasRef.value || !renderer || !camera || !effectComposer) return;

  const width = whirlpoolCanvasContainerRef.value!.clientWidth;
  const height = whirlpoolCanvasContainerRef.value!.clientHeight;

  // Update camera
  camera.aspect = width / height;
  camera.updateProjectionMatrix();

  // Update renderer size
  renderer.setSize(width, height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  // Update effect composer size
  effectComposer.setSize(width, height);
}

onUnmounted(() => {
  window.removeEventListener("mousemove", onPointerMove);
  window.removeEventListener("resize", onWindowResize);
});
</script>

```
::

## Examples

Without blur and overlay

```vue
<template>
  <ParticleWhirlpoolBg class="h-96" />
</template>

```

With `particleCount` 500

```vue
<template>
  <ParticleWhirlpoolBg
    class="h-96"
    :particle-count="500"
  />
</template>

```

## API

| Prop Name       | Type     | Default | Description                                                     |
| --------------- | -------- | ------- | --------------------------------------------------------------- |
| `class`         | `string` | `""`    | Additional CSS classes for custom styling.                      |
| `blur`          | `number` | `0`     | Amount of blur to apply to the background, specified in pixels. |
| `particleCount` | `number` | `2000`  | Number of particles in the whirlpool animation.                 |

## Features

- **Interactive Whirlpool Animation**: Renders a captivating whirlpool effect with particles that respond to mouse and touch interactions.

- **Customizable Particle Count**: Adjust the `particleCount` prop to control the number of particles in the animation.

- **Dynamic Blur Effect**: Use the `blur` prop to apply a blur effect over the background, enhancing the visual depth.

- **Slot Support**: Overlay additional content on top of the animation using the default slot.

- **Responsive Design**: The component adjusts to fit the width and height of its parent container, ensuring compatibility across different screen sizes.

## Credits

- Built with the [Three.js](https://threejs.org/) library for 3D rendering and animations.

- Inspired by [TroisJs](https://troisjs.github.io/examples/demos/3.html)

URL: https://inspira-ui.com/components/backgrounds/particles-bg

---
title: Particles Background
description: Particles can add a dynamic and engaging element to your website's visuals. They help create a feeling of depth, motion, and interaction, making the site more visually appealing.
---

```vue
<template>
  <div
    class="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
  >
    <span
      class="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"
    >
      Particles
    </span>
    <ParticlesBg
      class="absolute inset-0"
      :quantity="100"
      :ease="100"
      :color="isDark ? '#FFF' : '#000'"
      :staticity="10"
      refresh
    />
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const isDark = computed(() => useColorMode().value == "dark");
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="particles-bg" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="canvasContainerRef"
    :class="$props.class"
    aria-hidden="true"
  >
    <canvas ref="canvasRef"></canvas>
  </div>
</template>

<script setup lang="ts">
import { useMouse, useDevicePixelRatio } from "@vueuse/core";
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from "vue";

type Circle = {
  x: number;
  y: number;
  translateX: number;
  translateY: number;
  size: number;
  alpha: number;
  targetAlpha: number;
  dx: number;
  dy: number;
  magnetism: number;
};

type Props = {
  color?: string;
  quantity?: number;
  staticity?: number;
  ease?: number;
  class?: string;
};

const props = withDefaults(defineProps<Props>(), {
  color: "#FFF",
  quantity: 100,
  staticity: 50,
  ease: 50,
  class: "",
});

const canvasRef = ref<HTMLCanvasElement | null>(null);
const canvasContainerRef = ref<HTMLDivElement | null>(null);
const context = ref<CanvasRenderingContext2D | null>(null);
const circles = ref<Circle[]>([]);
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 });
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 });
const { x: mouseX, y: mouseY } = useMouse();
const { pixelRatio } = useDevicePixelRatio();

const color = computed(() => {
  // Remove the leading '#' if it's present
  let hex = props.color.replace(/^#/, "");

  // If the hex code is 3 characters, expand it to 6 characters
  if (hex.length === 3) {
    hex = hex
      .split("")
      .map((char) => char + char)
      .join("");
  }

  // Parse the r, g, b values from the hex string
  const bigint = parseInt(hex, 16);
  const r = (bigint >> 16) & 255; // Extract the red component
  const g = (bigint >> 8) & 255; // Extract the green component
  const b = bigint & 255; // Extract the blue component

  // Return the RGB values as a string separated by spaces
  return `${r} ${g} ${b}`;
});

onMounted(() => {
  if (canvasRef.value) {
    context.value = canvasRef.value.getContext("2d");
  }

  initCanvas();
  animate();
  window.addEventListener("resize", initCanvas);
});

onBeforeUnmount(() => {
  window.removeEventListener("resize", initCanvas);
});

watch([mouseX, mouseY], () => {
  onMouseMove();
});

function initCanvas() {
  resizeCanvas();
  drawParticles();
}

function onMouseMove() {
  if (canvasRef.value) {
    const rect = canvasRef.value.getBoundingClientRect();
    const { w, h } = canvasSize;
    const x = mouseX.value - rect.left - w / 2;
    const y = mouseY.value - rect.top - h / 2;

    const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
    if (inside) {
      mouse.x = x;
      mouse.y = y;
    }
  }
}

function resizeCanvas() {
  if (canvasContainerRef.value && canvasRef.value && context.value) {
    circles.value.length = 0;
    canvasSize.w = canvasContainerRef.value.offsetWidth;
    canvasSize.h = canvasContainerRef.value.offsetHeight;
    canvasRef.value.width = canvasSize.w * pixelRatio.value;
    canvasRef.value.height = canvasSize.h * pixelRatio.value;
    canvasRef.value.style.width = canvasSize.w + "px";
    canvasRef.value.style.height = canvasSize.h + "px";
    context.value.scale(pixelRatio.value, pixelRatio.value);
  }
}

function circleParams(): Circle {
  const x = Math.floor(Math.random() * canvasSize.w);
  const y = Math.floor(Math.random() * canvasSize.h);
  const translateX = 0;
  const translateY = 0;
  const size = Math.floor(Math.random() * 2) + 1;
  const alpha = 0;
  const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
  const dx = (Math.random() - 0.5) * 0.2;
  const dy = (Math.random() - 0.5) * 0.2;
  const magnetism = 0.1 + Math.random() * 4;
  return {
    x,
    y,
    translateX,
    translateY,
    size,
    alpha,
    targetAlpha,
    dx,
    dy,
    magnetism,
  };
}

function drawCircle(circle: Circle, update = false) {
  if (context.value) {
    const { x, y, translateX, translateY, size, alpha } = circle;
    context.value.translate(translateX, translateY);
    context.value.beginPath();
    context.value.arc(x, y, size, 0, 2 * Math.PI);
    context.value.fillStyle = `rgba(${color.value.split(" ").join(", ")}, ${alpha})`;
    context.value.fill();
    context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0);

    if (!update) {
      circles.value.push(circle);
    }
  }
}

function clearContext() {
  if (context.value) {
    context.value.clearRect(0, 0, canvasSize.w, canvasSize.h);
  }
}

function drawParticles() {
  clearContext();
  const particleCount = props.quantity;
  for (let i = 0; i < particleCount; i++) {
    const circle = circleParams();
    drawCircle(circle);
  }
}

function remapValue(
  value: number,
  start1: number,
  end1: number,
  start2: number,
  end2: number,
): number {
  const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
  return remapped > 0 ? remapped : 0;
}

function animate() {
  clearContext();
  circles.value.forEach((circle, i) => {
    // Handle the alpha value
    const edge = [
      circle.x + circle.translateX - circle.size, // distance from left edge
      canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
      circle.y + circle.translateY - circle.size, // distance from top edge
      canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
    ];

    const closestEdge = edge.reduce((a, b) => Math.min(a, b));
    const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));

    if (remapClosestEdge > 1) {
      circle.alpha += 0.02;
      if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
    } else {
      circle.alpha = circle.targetAlpha * remapClosestEdge;
    }

    circle.x += circle.dx;
    circle.y += circle.dy;
    circle.translateX +=
      (mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease;
    circle.translateY +=
      (mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease;

    // circle gets out of the canvas
    if (
      circle.x < -circle.size ||
      circle.x > canvasSize.w + circle.size ||
      circle.y < -circle.size ||
      circle.y > canvasSize.h + circle.size
    ) {
      // remove the circle from the array
      circles.value.splice(i, 1);
      // create a new circle
      const newCircle = circleParams();
      drawCircle(newCircle);
      // update the circle position
    } else {
      drawCircle(
        {
          ...circle,
          x: circle.x,
          y: circle.y,
          translateX: circle.translateX,
          translateY: circle.translateY,
          alpha: circle.alpha,
        },
        true,
      );
    }
  });
  window.requestAnimationFrame(animate);
}
</script>

```

## API

| Prop Name   | Type     | Default | Description                                                                                                 |
| ----------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `color`     | `string` | `#FFF`  | Hexadecimal color code used for particles. Supports 3 or 6 character hex codes.                             |
| `quantity`  | `number` | `100`   | The number of particles to generate and display on the canvas.                                              |
| `staticity` | `number` | `50`    | Determines how much the particles move based on the mouse's proximity. Higher values reduce movement.       |
| `ease`      | `number` | `50`    | Controls the easing effect of particle movement; lower values make particles follow the mouse more closely. |

## Credits

- Credits to [Magic UI](https://magicui.design/docs/components/particles) for this fantastic component.
- Credit to [Prodromos Pantos](https://github.com/prpanto) for porting the original component to Vue & Nuxt.

URL: https://inspira-ui.com/components/backgrounds/pattern-background

---
title: Pattern Background
description: Simple animated pattern background to make your sections stand out.
---

Grid background with dot
```vue
<template>
  <PatternBackground
    :animate="true"
    :direction="PATTERN_BACKGROUND_DIRECTION.TopRight"
    :variant="PATTERN_BACKGROUND_VARIANT.Dot"
    class="flex h-[36rem] w-full items-center justify-center"
    :speed="PATTERN_BACKGROUND_SPEED.Slow"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Dot Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_SPEED,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="pattern-background" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="PatternBackground.vue" language="vue" componentName="PatternBackground" type="ui" id="pattern-background"}
:CodeViewerTab{filename="index.ts" language="typescript" componentName="index" type="ui" id="pattern-background" extension="ts"}

::

## Examples

Grid background with big dot and ellipse on top
```vue
<template>
  <PatternBackground
    :animate="true"
    :direction="PATTERN_BACKGROUND_DIRECTION.Bottom"
    :variant="PATTERN_BACKGROUND_VARIANT.BigDot"
    class="flex h-[36rem] w-full items-center justify-center"
    :speed="PATTERN_BACKGROUND_SPEED.Slow"
    :mask="PATTERN_BACKGROUND_MASK.EllipseTop"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Big Dot Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_MASK,
  PATTERN_BACKGROUND_SPEED,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

Grid background without animation
```vue
<template>
  <PatternBackground
    :direction="PATTERN_BACKGROUND_DIRECTION.TopRight"
    :variant="PATTERN_BACKGROUND_VARIANT.Grid"
    class="flex h-[36rem] w-full items-center justify-center"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Grid Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

Small grid background with animation
```vue
<template>
  <PatternBackground
    :animate="true"
    :direction="PATTERN_BACKGROUND_DIRECTION.Right"
    :variant="PATTERN_BACKGROUND_VARIANT.Grid"
    class="flex h-[36rem] w-full items-center justify-center"
    size="xs"
    :speed="PATTERN_BACKGROUND_SPEED.Slow"
  >
    <p
      class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-4xl font-bold text-transparent sm:text-5xl"
    >
      Small Grid Background
    </p>
  </PatternBackground>
</template>

<script setup lang="ts">
import {
  PATTERN_BACKGROUND_DIRECTION,
  PATTERN_BACKGROUND_SPEED,
  PATTERN_BACKGROUND_VARIANT,
} from "../../ui/pattern-background";
</script>

```

## API

| Prop Name   | Type                                                                                                   | Default   | Description                                                                                                                                                    |
| ----------- | ------------------------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `animate`   | `boolean`                                                                                              | `false`   | Set `true` if you want to animate the background.                                                                                                              |
| `direction` | `top` \| `bottom` \| `left` \| `right` \| `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | `top`     | Direction of the animation movement. You can use the const `PATTERN_BACKGROUND_DIRECTION.`                                                                     |
| `direction` | `grid` \| `dot`                                                                                        | `grid`    | Type of pattern. You can use the const `PATTERN_BACKGROUND_VARIANT.`                                                                                           |
| `size`      | `xs` \| `sm` \| `md` \| `lg`                                                                           | `md`      | Size of the background pattern.                                                                                                                                |
| `mask`      | `ellipse` \| `ellipse-top`                                                                             | `ellipse` | Add a mask over the background pattern. You can use the const `PATTERN_BACKGROUND_MASK.`                                                                       |
| `speed`     | `number`                                                                                               | `10000`   | Duration of the animation in `ms`, the bigger it is, the slower the animation. (`20000` slower than `5000`). You can use the const `PATTERN_BACKGROUND_SPEED.` |

### Custom variants, values and constants

You can customize your needs directly within the `index.ts` file. See code below.

## Credits

- Inspired by [Magic UI's Dot Pattern](https://magicui.design/docs/components/dot-pattern) component.
- Inspired by [Magic UI's Grid Pattern](https://magicui.design/docs/components/grid-pattern) component.
- Inspired by [Magic UI's Animated Grid Pattern](https://magicui.design/docs/components/animated-grid-pattern) component.
- Credits to [Nathan De Pachtere](https://nathandepachtere.com/) for porting this component.

URL: https://inspira-ui.com/components/backgrounds/ripple

---
title: Ripple
description: An animated ripple effect typically used behind elements to emphasize them.
---

```vue
<template>
  <div
    class="relative flex h-[450px] w-full flex-col items-center justify-center overflow-hidden rounded-lg lg:w-full md:w-full"
  >
    <p
      class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white"
    >
      Ripple
    </p>
    <Ripple
      class="bg-white/5 [mask-image:linear-gradient(to_bottom,white,transparent)]"
      circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-[#fff]/25 rounded-full"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="ripple" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Ripple.vue" language="vue" componentName="Ripple" type="ui" id="ripple"}
:CodeViewerTab{filename="RippleCircle.vue" language="vue" componentName="RippleCircle" type="ui" id="ripple"}
:CodeViewerTab{filename="RippleContainer.vue" language="vue" componentName="RippleContainer" type="ui" id="ripple"}
::

## Examples

Only lines

```vue
<template>
  <div
    class="relative flex h-[450px] w-full flex-col items-center justify-center overflow-hidden lg:w-full md:w-full"
  >
    <p
      class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white"
    >
      Lines
    </p>
    <small>Only</small>
    <Ripple circle-class="border-[hsl(var(--primary))] rounded-full" />
  </div>
</template>

```

Squared

```vue
<template>
  <div
    class="relative flex h-[450px] w-full flex-col items-center justify-center overflow-hidden rounded-lg lg:w-full md:w-full"
  >
    <p
      class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white"
    >
      Squared
    </p>
    <Ripple
      class="bg-white/5 [mask-image:linear-gradient(to_bottom,white,transparent)]"
      circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-[#fff]/25 rounded-md"
    />
  </div>
</template>

```

Blobed

```vue
<template>
  <div
    class="relative flex h-[450px] w-full flex-col items-center justify-center overflow-hidden rounded-lg lg:w-full md:w-full"
  >
    <p
      class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white"
    >
      Blobs
    </p>
    <small>Are awesome</small>
    <Ripple
      class="bg-white/5 [mask-image:linear-gradient(to_bottom,white,transparent)]"
      circle-class="border-[hsl(var(--primary))] bg-primary/25 blobed"
    />
  </div>
</template>

<style scoped>
:deep(.blobed) {
  border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
}
</style>

```

## API

| Prop Name                     | Type     | Default     | Description                                                            |
| ----------------------------- | -------- | ----------- | ---------------------------------------------------------------------- |
| `baseCircleSize`              | `number` | `210`       | The size of the main circle in pixels.                                 |
| `baseCircleOpacity`           | `number` | `0.24`      | The opacity of the main circle.                                        |
| `spaceBetweenCircle`          | `number` | `70`        | The space between each ripple circle in pixels.                        |
| `circleOpacityDowngradeRatio` | `number` | `0.03`      | The rate at which opacity decreases for each successive ripple circle. |
| `circleClass`                 | `string` | `undefined` | CSS class name(s) for additional styling of circles.                   |
| `waveSpeed`                   | `number` | `80`        | The animation speed for the wave effect, measured in ms.               |
| `numberOfCircles`             | `number` | `7`         | The number of ripple circles to render.                                |

## Credits

- Credits to [Magic UI](https://magicui.design/docs/components/ripple).
- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for porting this component.
- Credits to [Nathan De Pachtere](https://nathandepachtere.com/) for updating this component.

URL: https://inspira-ui.com/components/backgrounds/snowfall-bg

---
title: Snowfall Background
description: A beautifully animated snowfall effect applied as a background.
---

```vue
<template>
  <div class="relative h-96 w-full">
    <SnowfallBg
      color="ADD8E6"
      class="absolute inset-0"
      :min-radius="0.2"
      :max-radius="5"
      :speed="0.5"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="snowfall-bg" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="canvasContainerRef"
    :class="$props.class"
    aria-hidden="true"
  >
    <canvas ref="canvasRef"></canvas>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, reactive, computed } from "vue";
import { useDevicePixelRatio } from "@vueuse/core";

type Snowflake = {
  x: number;
  y: number;
  size: number;
  alpha: number;
  dx: number; // Horizontal drift
  dy: number; // Vertical fall speed
};

type Props = {
  color?: string;
  quantity?: number;
  speed?: number;
  maxRadius?: number;
  minRadius?: number;
  class?: string;
};

const props = withDefaults(defineProps<Props>(), {
  color: "#FFF",
  quantity: 100,
  speed: 1, // Controls how fast the snowflakes fall
  maxRadius: 3, // Default max radius
  minRadius: 1, // Default min radius
  class: "",
});

const canvasRef = ref<HTMLCanvasElement | null>(null);
const canvasContainerRef = ref<HTMLDivElement | null>(null);
const context = ref<CanvasRenderingContext2D | null>(null);
const snowflakes = ref<Snowflake[]>([]);
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 });
const { pixelRatio } = useDevicePixelRatio();

const color = computed(() => {
  const hex = props.color.replace(/^#/, "").padStart(6, "0");
  const bigint = parseInt(hex, 16);
  const r = (bigint >> 16) & 255;
  const g = (bigint >> 8) & 255;
  const b = bigint & 255;
  return `${r} ${g} ${b}`;
});

onMounted(() => {
  if (canvasRef.value) {
    context.value = canvasRef.value.getContext("2d");
  }
  initCanvas();
  animate();
  window.addEventListener("resize", initCanvas);
});

onBeforeUnmount(() => {
  window.removeEventListener("resize", initCanvas);
});

function initCanvas() {
  resizeCanvas();
  createSnowflakes();
}

function resizeCanvas() {
  if (canvasContainerRef.value && canvasRef.value && context.value) {
    snowflakes.value.length = 0;
    canvasSize.w = canvasContainerRef.value.offsetWidth;
    canvasSize.h = canvasContainerRef.value.offsetHeight;
    canvasRef.value.width = canvasSize.w * pixelRatio.value;
    canvasRef.value.height = canvasSize.h * pixelRatio.value;
    canvasRef.value.style.width = `${canvasSize.w}px`;
    canvasRef.value.style.height = `${canvasSize.h}px`;
    context.value.scale(pixelRatio.value, pixelRatio.value);
  }
}

function createSnowflakes() {
  for (let i = 0; i < props.quantity; i++) {
    const snowflake = createSnowflake();
    snowflakes.value.push(snowflake);
  }
}

function createSnowflake(): Snowflake {
  const x = Math.random() * canvasSize.w;
  const y = Math.random() * canvasSize.h;
  const size = Math.random() * (props.maxRadius! - props.minRadius!) + props.minRadius!; // Random size between min and max radius
  const alpha = Math.random() * 0.5 + 0.5; // Opacity between 0.5 and 1
  const dx = (Math.random() - 0.5) * 0.5; // Slight horizontal drift
  const dy = Math.random() * 0.25 + props.speed; // Falling speed

  return { x, y, size, alpha, dx, dy };
}

function drawSnowflake(snowflake: Snowflake) {
  if (context.value) {
    const { x, y, size, alpha } = snowflake;
    context.value.beginPath();
    context.value.arc(x, y, size, 0, Math.PI * 2);
    context.value.fillStyle = `rgba(${color.value.split(" ").join(", ")}, ${alpha})`;
    context.value.fill();
  }
}

function animate() {
  if (context.value) {
    context.value.clearRect(0, 0, canvasSize.w, canvasSize.h);
  }

  snowflakes.value.forEach((snowflake) => {
    snowflake.x += snowflake.dx; // Drift horizontally
    snowflake.y += snowflake.dy; // Fall down

    // Reset snowflake when it moves out of the canvas
    if (snowflake.y > canvasSize.h) {
      snowflake.y = -snowflake.size; // Reset to the top
      snowflake.x = Math.random() * canvasSize.w; // Random horizontal position
    }

    drawSnowflake(snowflake);
  });

  requestAnimationFrame(animate);
}
</script>

```

## API

| Prop Name   | Type     | Default | Description                                               |
| ----------- | -------- | ------- | --------------------------------------------------------- |
| `color`     | `string` | `#FFF`  | Color of the snowflakes in hexadecimal format.            |
| `quantity`  | `number` | `100`   | Number of snowflakes to display.                          |
| `speed`     | `number` | `1`     | Speed at which snowflakes fall.                           |
| `maxRadius` | `number` | `3`     | Maximum radius of snowflakes.                             |
| `minRadius` | `number` | `1`     | Minimum radius of snowflakes.                             |
| `class`     | `string` | `null`  | Additional CSS classes to apply to the container element. |

## Credits

- Inspired by natural snowfall effects.

URL: https://inspira-ui.com/components/backgrounds/sparkles

---
title: Sparkles
description: A configurable sparkles component that can be used as a background or as a standalone component.
---

```vue
<template>
  <div
    class="flex h-[40rem] w-full flex-col items-center justify-center overflow-hidden rounded-md bg-white dark:bg-black"
  >
    <h1
      class="relative z-20 text-center text-3xl font-bold text-black lg:text-9xl md:text-7xl dark:text-white"
    >
      Inspira UI
    </h1>
    <div class="relative h-40 w-[40rem]">
      <div
        class="absolute inset-x-20 top-0 h-[2px] w-3/4 bg-gradient-to-r from-transparent via-indigo-500 to-transparent blur-sm"
      />
      <div
        class="absolute inset-x-20 top-0 h-px w-3/4 bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
      />
      <div
        class="absolute inset-x-60 top-0 h-[5px] w-1/4 bg-gradient-to-r from-transparent via-sky-500 to-transparent blur-sm"
      />
      <div
        class="absolute inset-x-60 top-0 h-px w-1/4 bg-gradient-to-r from-transparent via-sky-500 to-transparent"
      />

      <Sparkles
        background="transparent"
        :min-size="0.4"
        :max-size="1.4"
        :particle-density="1200"
        class="size-full"
        :particle-color="particlesColor"
      />

      <div
        class="absolute inset-0 size-full bg-white [mask-image:radial-gradient(350px_200px_at_top,transparent_20%,white)] dark:bg-black"
      ></div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const particlesColor = computed(() => (useColorMode().value === "dark" ? "#FFFFFF" : "#000000"));
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="sparkles" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="containerRef"
    class="relative size-full overflow-hidden will-change-transform"
    :style="{ background }"
  >
    <canvas
      ref="canvasRef"
      class="absolute inset-0 size-full"
    />
  </div>
</template>

<script setup lang="ts">
import { useRafFn, templateRef } from "@vueuse/core";
import { ref, onMounted, onBeforeUnmount } from "vue";

interface Props {
  background?: string;
  particleColor?: string;
  minSize?: number;
  maxSize?: number;
  speed?: number;
  particleDensity?: number;
}

interface Particle {
  x: number;
  y: number;
  size: number;
  opacity: number;
  vx: number;
  vy: number;
  phase: number;
  phaseSpeed: number;
}

const props = withDefaults(defineProps<Props>(), {
  background: "#0d47a1",
  particleColor: "#ffffff",
  minSize: 1,
  maxSize: 3,
  speed: 4,
  particleDensity: 120,
});

const containerRef = templateRef<HTMLElement | null>("containerRef");
const canvasRef = templateRef<HTMLCanvasElement | null>("canvasRef");
const particles = ref<Particle[]>([]);
const ctx = ref<CanvasRenderingContext2D | null>(null);

// Adjust canvas size on mount and resize
function resizeCanvas() {
  if (!canvasRef.value || !containerRef.value) return;

  const dpr = window.devicePixelRatio || 1;
  const rect = containerRef.value.getBoundingClientRect();

  canvasRef.value.width = rect.width * dpr;
  canvasRef.value.height = rect.height * dpr;

  if (ctx.value) {
    ctx.value.scale(dpr, dpr);
  }
}

function generateParticles(): void {
  const newParticles: Particle[] = [];
  const count = props.particleDensity;

  for (let i = 0; i < count; i++) {
    const baseSpeed = 0.05;
    const speedVariance = Math.random() * 0.3 + 0.7;

    newParticles.push({
      x: Math.random() * 100,
      y: Math.random() * 100,
      size: Math.random() * (props.maxSize - props.minSize) + props.minSize,
      opacity: Math.random() * 0.5 + 0.3,
      vx: (Math.random() - 0.5) * baseSpeed * speedVariance * props.speed,
      vy: ((Math.random() - 0.5) * baseSpeed - baseSpeed * 0.3) * speedVariance * props.speed,
      phase: Math.random() * Math.PI * 2,
      phaseSpeed: 0.015,
    });
  }

  particles.value = newParticles;
}

function updateAndDrawParticles() {
  if (!ctx.value || !canvasRef.value) return;

  const canvas = canvasRef.value;
  ctx.value.clearRect(0, 0, canvas.width, canvas.height);

  particles.value = particles.value.map((particle) => {
    let newX = particle.x + particle.vx;
    let newY = particle.y + particle.vy;

    if (newX < -2) newX = 102;
    if (newX > 102) newX = -2;
    if (newY < -2) newY = 102;
    if (newY > 102) newY = -2;

    const newPhase = (particle.phase + particle.phaseSpeed) % (Math.PI * 2);
    const opacity = 0.3 + (Math.sin(newPhase) * 0.3 + 0.3);

    // Draw particle
    ctx.value!.beginPath();
    ctx.value!.arc(
      (newX * canvas.width) / 100,
      (newY * canvas.height) / 100,
      particle.size,
      0,
      Math.PI * 2,
    );
    ctx.value!.fillStyle = `${props.particleColor}${Math.floor(opacity * 255)
      .toString(16)
      .padStart(2, "0")}`;
    ctx.value!.fill();

    return {
      ...particle,
      x: newX,
      y: newY,
      phase: newPhase,
      opacity,
    };
  });
}

const { pause, resume } = useRafFn(updateAndDrawParticles, { immediate: false });

// Handle window resize
let resizeObserver: ResizeObserver | undefined;

onMounted(() => {
  if (!canvasRef.value) return;

  ctx.value = canvasRef.value.getContext("2d");
  resizeCanvas();
  generateParticles();

  // Set up resize observer
  resizeObserver = new ResizeObserver(resizeCanvas);
  if (containerRef.value) {
    resizeObserver.observe(containerRef.value);
  }

  resume();
});

onBeforeUnmount(() => {
  pause();
  if (resizeObserver && containerRef.value) {
    resizeObserver.unobserve(containerRef.value);
  }
});
</script>

```

## Examples

Sparkles Full Page

```vue
<template>
  <div
    class="relative flex h-[40rem] w-full flex-col items-center justify-center overflow-hidden rounded-md bg-white dark:bg-black"
  >
    <div class="absolute inset-0 h-screen w-full">
      <Sparkles
        background="transparent"
        :min-size="0.8"
        :max-size="2"
        :particle-density="800"
        class="size-full"
        :particle-color="particlesColor"
      />
    </div>
    <h1
      class="relative z-20 text-center text-3xl font-bold text-black lg:text-6xl md:text-7xl dark:text-white"
    >
      Build great products
    </h1>
  </div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const particlesColor = computed(() => (useColorMode().value === "dark" ? "#FFFFFF" : "#000000"));
</script>

```

## API

| Prop Name         | Type     | Default     | Description                                                                            |
| ----------------- | -------- | ----------- | -------------------------------------------------------------------------------------- |
| `background`      | `string` | `'#0d47a1'` | Background color of the container. Use 'transparent' to see through to parent element. |
| `particleColor`   | `string` | `'#ffffff'` | Color of the particles. Accepts any valid CSS color value.                             |
| `minSize`         | `number` | `1`         | Minimum size of particles in pixels.                                                   |
| `maxSize`         | `number` | `3`         | Maximum size of particles in pixels.                                                   |
| `speed`           | `number` | `4`         | Movement speed multiplier. Higher values create faster movement.                       |
| `particleDensity` | `number` | `120`       | Number of particles to render. Higher values create denser particle fields.            |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for porting this component.

- Ported from [Aceternity UI's Sparkles](https://ui.aceternity.com/components/sparkles).

URL: https://inspira-ui.com/components/backgrounds/tetris

---
title: Tetris
description: Tetris background component, you can even click on a block to eliminate it.
---

```vue
<template>
  <ClientOnly>
    <Tetris
      class="h-[500px] w-full [mask-image:radial-gradient(450px_circle_at_center,#00C16A,transparent)]"
      :base="15"
      square-color="#00C16A"
    />
  </ClientOnly>
</template>

<script lang="ts" setup></script>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

::alert{type="info"}
**Note:** This component requires `theme-colors` as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="tetris" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install theme-colors
```

```bash [pnpm]
pnpm install theme-colors
```

```bash [bun]
bun add theme-colors
```

```bash [yarn]
yarn add theme-colors
```

::

Copy and paste the following code

```vue
<template>
  <Transition
    appear
    name="fade"
  >
    <div
      :style="{
        '--cell': `${width / cols}px`,
        '--rows': rows - 1,
      }"
      :class="cn('relative w-full ', props.class)"
    >
      <div
        ref="el"
        class="absolute inset-0 grid auto-rows-[--cell] justify-center -space-y-px"
      >
        <div
          v-for="(row, rowIndex) in grid"
          :key="rowIndex"
          class="grid flex-1 auto-cols-[--cell] grid-flow-col -space-x-px"
        >
          <div
            v-for="(cell, cellIndex) in row"
            :key="cellIndex"
            :style="{
              '--border-color': theme[100],
              '--dark-border-color': theme[900],
            }"
            class="relative border border-[--border-color] dark:border-[--dark-border-color]"
          >
            <div
              :style="{
                '--square-color': theme[500],
                '--square-hover-color': theme[400],
                '--dark-square-color': theme[700],
                '--dark-square-hover-color': theme[600],
              }"
              class="absolute inset-0 bg-[--square-color] opacity-0 transition-opacity duration-1000 will-change-[opacity] hover:bg-[--square-hover-color] dark:bg-[--dark-square-color] dark:hover:bg-[--dark-square-hover-color]"
              :class="[cell && 'cursor-pointer opacity-60']"
              @click="cell && removeCell(rowIndex, cellIndex)"
            />
          </div>
        </div>
      </div>
    </div>
  </Transition>
</template>

<script setup lang="ts">
import { useElementSize } from "@vueuse/core";
import { cn } from "@/lib/utils";
import { getColors } from "theme-colors";
import { ref, onMounted, onUnmounted, watch } from "vue";

interface Props {
  class?: string;
  squareColor: string;
  base: number;
}

const props = withDefaults(defineProps<Props>(), {
  base: 10,
});

const theme = getColors(props.squareColor);

const el = ref(null);
const grid = ref<(boolean | null)[][]>([]);
const rows = ref(0);
const cols = ref(0);

const { width, height } = useElementSize(el);

function createGrid() {
  grid.value = [];

  for (let i = 0; i < rows.value; i++) {
    grid.value.push(new Array(cols.value).fill(null));
  }
}

function createNewCell() {
  const x = Math.floor(Math.random() * cols.value);

  grid.value[0][x] = true;
}

function moveCellsDown() {
  for (let row = rows.value - 1; row >= 0; row--) {
    for (let col = 0; col < cols.value; col++) {
      const cell = grid.value[row][col];
      const nextCell = Array.isArray(grid.value[row + 1]) ? grid.value[row + 1][col] : cell;
      if (cell !== null && nextCell === null) {
        grid.value[row + 1][col] = grid.value[row][col];
        grid.value[row][col] = null;
      }
    }
  }

  setTimeout(() => {
    const isFilled = grid.value[rows.value - 1].every((cell) => cell !== null);
    if (Array.isArray(grid.value[rows.value]) && isFilled) {
      for (let col = 0; col < cols.value; col++) {
        grid.value[rows.value][col] = null;
      }
    }
  }, 500);
}

function clearColumn() {
  const isFilled = grid.value[rows.value - 1].every((cell) => cell === true);
  if (!isFilled) return;

  for (let col = 0; col < cols.value; col++) {
    grid.value[rows.value - 1][col] = null;
  }
}

function removeCell(row: number, col: number) {
  grid.value[row][col] = null;
}

function calcGrid() {
  const cell = width.value / props.base;

  rows.value = Math.floor(height.value / cell);
  cols.value = Math.floor(width.value / cell);

  createGrid();
}

watch(width, calcGrid);

// eslint-disable-next-line no-undef
let intervalId: NodeJS.Timeout | undefined;
// eslint-disable-next-line no-undef
let timeoutId: NodeJS.Timeout | undefined;

onMounted(() => {
  timeoutId = setTimeout(calcGrid, 50);

  intervalId = setInterval(() => {
    clearColumn();
    moveCellsDown();
    createNewCell();
  }, 1000);
});

onUnmounted(() => {
  clearInterval(intervalId);
  clearTimeout(timeoutId);
});
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

```
::

## Example

Without Blur or overlay

```vue
<template>
  <BubblesBg class="h-96 w-full" />
</template>

```

## API

| Prop Name      | Type     | Default | Description                                    |
| -------------- | -------- | ------- | ---------------------------------------------- |
| `class`        | `string` | `""`    | Additional class names to style the component. |
| `base`         | `number` | `10`    | How many blocks do you have in a row.          |
| `square-color` | `string` | `""`    | Square color.                                  |

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Inspired and ported from [Nuxt UI Home](https://ui.nuxt.com/).

URL: https://inspira-ui.com/components/backgrounds/vortex

---
title: Vortex Background
description: A wavy, swirly, vortex background ideal for CTAs and backgrounds.
---

```vue
<template>
  <div class="mx-auto h-[30rem] w-[calc(100%-4rem)] overflow-hidden rounded-md">
    <Vortex
      background-color="black"
      class="flex size-full flex-col items-center justify-center px-2 py-4 md:px-10"
    >
      <h2 class="text-center text-2xl font-bold text-white md:text-6xl">The hell is this?</h2>
      <p class="mt-6 max-w-xl text-center text-sm text-white md:text-2xl">
        This is chemical burn. It&apos;ll hurt more than you&apos;ve ever been burned and
        you&apos;ll have a scar.
      </p>
      <div class="mt-6 flex flex-col items-center gap-4 sm:flex-row">
        <button
          class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
        >
          Order now
        </button>
        <button
          class="rounded-md bg-white px-4 py-2 text-sm font-medium text-black transition-colors hover:bg-gray-100"
        >
          Watch trailer
        </button>
      </div>
    </Vortex>
  </div>
</template>

```

::alert{type="info"}
**Note:** This component requires `simplex-noise` as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="vortex" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install simplex-noise
```

```bash [pnpm]
pnpm install simplex-noise
```

```bash [bun]
bun add simplex-noise
```

```bash [yarn]
yarn add simplex-noise
```

::

Copy and paste the following code

```vue
<template>
  <div :class="cn('relative h-full w-full', props.containerClass)">
    <Motion
      ref="containerRef"
      as="div"
      :initial="{ opacity: 0 }"
      :animate="{ opacity: 1 }"
      class="absolute inset-0 z-0 flex size-full items-center justify-center bg-transparent"
    >
      <canvas ref="canvasRef"></canvas>
    </Motion>

    <div :class="cn('relative z-10', props.class)">
      <slot />
    </div>
  </div>
</template>

<script setup lang="ts">
import { createNoise3D } from "simplex-noise";
import { onMounted, onUnmounted } from "vue";
import { templateRef } from "@vueuse/core";
import { cn } from "@/lib/utils";

const TAU = 2 * Math.PI;
const BASE_TTL = 50;
const RANGE_TTL = 150;
const PARTICLE_PROP_COUNT = 9;
const RANGE_HUE = 100;
const NOISE_STEPS = 3;
const X_OFF = 0.00125;
const Y_OFF = 0.00125;
const Z_OFF = 0.0005;

interface VortexProps {
  class?: string;
  containerClass?: string;
  particleCount?: number;
  rangeY?: number;
  baseHue?: number;
  baseSpeed?: number;
  rangeSpeed?: number;
  baseRadius?: number;
  rangeRadius?: number;
  backgroundColor?: string;
}

const props = withDefaults(defineProps<VortexProps>(), {
  particleCount: 700,
  rangeY: 100,
  baseSpeed: 0.0,
  rangeSpeed: 1.5,
  baseRadius: 1,
  rangeRadius: 2,
  baseHue: 220,
  backgroundColor: "#000000",
});

const tick = ref<number>(0);
const animationFrame = ref<number | null>(null);
const particleProps = shallowRef<Float32Array | null>(null);
const center = ref<[number, number]>([0, 0]);
const ctx = shallowRef<CanvasRenderingContext2D | null>(null);

const canvasRef = templateRef<HTMLCanvasElement | null>("canvasRef");
const containerRef = templateRef<HTMLElement | null>("containerRef");

const particleCache = {
  x: 0,
  y: 0,
  vx: 0,
  vy: 0,
  life: 0,
  ttl: 0,
  speed: 0,
  radius: 0,
  hue: 0,
};

const noise3D = createNoise3D();

function rand(n: number) {
  return n * Math.random();
}
function randRange(n: number): number {
  return n - rand(2 * n);
}
function fadeInOut(t: number, m: number): number {
  const hm = 0.5 * m;
  return Math.abs(((t + hm) % m) - hm) / hm;
}
function lerp(n1: number, n2: number, speed: number): number {
  return (1 - speed) * n1 + speed * n2;
}

function initParticle(i: number) {
  if (!particleProps.value || !canvasRef.value) return;

  const canvas = canvasRef.value;
  particleCache.x = rand(canvas.width);
  particleCache.y = center.value[1] + randRange(props.rangeY);
  particleCache.vx = 0;
  particleCache.vy = 0;
  particleCache.life = 0;
  particleCache.ttl = BASE_TTL + rand(RANGE_TTL);
  particleCache.speed = props.baseSpeed + rand(props.rangeSpeed);
  particleCache.radius = props.baseRadius + rand(props.rangeRadius);
  particleCache.hue = props.baseHue + rand(RANGE_HUE);

  particleProps.value.set(
    [
      particleCache.x,
      particleCache.y,
      particleCache.vx,
      particleCache.vy,
      particleCache.life,
      particleCache.ttl,
      particleCache.speed,
      particleCache.radius,
      particleCache.hue,
    ],
    i,
  );
}

function updateParticle(i: number) {
  if (!particleProps.value || !canvasRef.value || !ctx.value) return;

  const canvas = canvasRef.value;
  const props = particleProps.value;
  const context = ctx.value;

  particleCache.x = props[i];
  particleCache.y = props[i + 1];
  particleCache.vx = props[i + 2];
  particleCache.vy = props[i + 3];
  particleCache.life = props[i + 4];
  particleCache.ttl = props[i + 5];
  particleCache.speed = props[i + 6];
  particleCache.radius = props[i + 7];
  particleCache.hue = props[i + 8];

  const n =
    noise3D(particleCache.x * X_OFF, particleCache.y * Y_OFF, tick.value * Z_OFF) *
    NOISE_STEPS *
    TAU;

  const nextVx = lerp(particleCache.vx, Math.cos(n), 0.5);
  const nextVy = lerp(particleCache.vy, Math.sin(n), 0.5);
  const nextX = particleCache.x + nextVx * particleCache.speed;
  const nextY = particleCache.y + nextVy * particleCache.speed;

  context.save();
  context.lineCap = "round";
  context.lineWidth = particleCache.radius;
  context.strokeStyle = `hsla(${particleCache.hue},100%,60%,${fadeInOut(
    particleCache.life,
    particleCache.ttl,
  )})`;
  context.beginPath();
  context.moveTo(particleCache.x, particleCache.y);
  context.lineTo(nextX, nextY);
  context.stroke();
  context.restore();

  props[i] = nextX;
  props[i + 1] = nextY;
  props[i + 2] = nextVx;
  props[i + 3] = nextVy;
  props[i + 4] = particleCache.life + 1;

  if (
    nextX > canvas.width ||
    nextX < 0 ||
    nextY > canvas.height ||
    nextY < 0 ||
    particleCache.life > particleCache.ttl
  ) {
    initParticle(i);
  }
}

function draw() {
  if (!canvasRef.value || !ctx.value || !particleProps.value) return;

  const canvas = canvasRef.value;
  const context = ctx.value;

  tick.value++;

  context.fillStyle = props.backgroundColor;
  context.fillRect(0, 0, canvas.width, canvas.height);

  for (let i = 0; i < particleProps.value.length; i += PARTICLE_PROP_COUNT) {
    updateParticle(i);
  }

  context.save();
  context.filter = "blur(8px) brightness(200%)";
  context.globalCompositeOperation = "lighter";
  context.drawImage(canvas, 0, 0);
  context.restore();

  context.save();
  context.filter = "blur(4px) brightness(200%)";
  context.globalCompositeOperation = "lighter";
  context.drawImage(canvas, 0, 0);
  context.restore();

  animationFrame.value = requestAnimationFrame(draw);
}

const handleResize = useDebounceFn(() => {
  if (!canvasRef.value) return;

  const canvas = canvasRef.value;
  const { innerWidth, innerHeight } = window;
  canvas.width = innerWidth;
  canvas.height = innerHeight;
  center.value = [0.5 * canvas.width, 0.5 * canvas.height];
}, 150);

onMounted(() => {
  const canvas = canvasRef.value;
  if (!canvas) return;

  ctx.value = canvas.getContext("2d");
  if (!ctx.value) return;

  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  center.value = [0.5 * canvas.width, 0.5 * canvas.height];

  const particlePropsLength = props.particleCount * PARTICLE_PROP_COUNT;
  particleProps.value = new Float32Array(particlePropsLength);

  for (let i = 0; i < particlePropsLength; i += PARTICLE_PROP_COUNT) {
    initParticle(i);
  }

  draw();
  window.addEventListener("resize", handleResize);
});

onUnmounted(() => {
  if (animationFrame.value) {
    cancelAnimationFrame(animationFrame.value);
  }
  window.removeEventListener("resize", handleResize);

  ctx.value = null;
  particleProps.value = null;
});
</script>

```
::

## Example

Full page demo usage

```vue
<template>
  <div class="mx-auto h-screen w-[calc(100%-4rem)] overflow-hidden rounded-md">
    <Vortex
      background-color="black"
      :range-y="800"
      :particle-count="500"
      :base-hue="120"
      class="flex size-full flex-col items-center justify-center px-2 py-4 md:px-10"
    >
      <h2 class="text-center text-2xl font-bold text-white md:text-6xl">The hell is this?</h2>
      <p class="mt-6 max-w-xl text-center text-sm text-white md:text-2xl">
        This is chemical burn. It&apos;ll hurt more than you&apos;ve ever been burned and
        you&apos;ll have a scar.
      </p>
      <div class="mt-6 flex flex-col items-center gap-4 sm:flex-row">
        <button
          class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
        >
          Order now
        </button>
        <button
          class="rounded-md bg-white px-4 py-2 text-sm font-medium text-black transition-colors hover:bg-gray-100"
        >
          Watch trailer
        </button>
      </div>
    </Vortex>
  </div>
</template>

```

## API

| Prop Name         | Type     | Default     | Description                                          |
| ----------------- | -------- | ----------- | ---------------------------------------------------- |
| `class`           | `string` |             | Optional className for styling the children wrapper. |
| `containerClass`  | `string` |             | Optional className for styling the container.        |
| `particleCount`   | `number` | `700`       | Number of particles to be generated.                 |
| `rangeY`          | `number` | `100`       | Vertical range for particle movement.                |
| `baseHue`         | `number` | `220`       | Base hue for particle color.                         |
| `baseSpeed`       | `number` | `0.0`       | Base speed for particle movement.                    |
| `rangeSpeed`      | `number` | `1.5`       | Range of speed variation for particles.              |
| `baseRadius`      | `number` | `1`         | Base radius of particles.                            |
| `rangeRadius`     | `number` | `2`         | Range of radius variation for particles.             |
| `backgroundColor` | `string` | `"#000000"` | Background color of the canvas.                      |

## Features

- **Slot Support**: Easily add any content inside the component using the default slot.

## Credits

- Credits to [Aceternity UI](https://ui.aceternity.com/components/vortex).
- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for porting this component.

URL: https://inspira-ui.com/components/backgrounds/warp-background

---
title: Warp Background
description: A container component that applies a warp animation effect to its children
---

```vue
<template>
  <div class="w-full">
    <WarpBackground>
      <div class="mx-auto w-72 rounded-lg border bg-card shadow-sm">
        <div class="flex flex-col gap-2 p-4">
          <h3 class="text-xl font-semibold leading-none tracking-tight">
            Congratulations on Your Promotion!
          </h3>
          <p class="text-sm text-gray-500 dark:text-gray-400">
            Your hard work and dedication have paid off. We&apos;re thrilled to see you take this
            next step in your career. Keep up the fantastic work!
          </p>
        </div>
      </div>
    </WarpBackground>
  </div>
</template>

<script lang="ts" setup></script>

<style></style>

```

## Install using CLI

```vue
<InstallationCli component-id="warp-background" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="WarpBackground.vue" language="vue" componentName="WarpBackground" type="ui" id="warp-background"}
:CodeViewerTab{filename="Beam.vue" language="vue" componentName="Beam" type="ui" id="warp-background"}

::

## API

| Prop Name      | Type     | Default                | Description                               |
| -------------- | -------- | ---------------------- | ----------------------------------------- |
| `perspective`  | `number` | `100`                  | The perspective of the warp animation     |
| `beamsPerSide` | `number` | `3`                    | The number of beams per side              |
| `beamSize`     | `number` | `5`                    | The size of the beams                     |
| `beamDelayMax` | `number` | `3`                    | The maximum delay of the beams in seconds |
| `beamDelayMin` | `number` | `0`                    | The minimum delay of the beams in seconds |
| `beamDuration` | `number` | `3`                    | The duration of the beams                 |
| `gridColor`    | `string` | `"hsl(var(--border))"` | The color of the grid lines               |

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Inspired and ported from [Magic UI WarpBackground](https://magicui.design/docs/components/warp-background).

URL: https://inspira-ui.com/components/backgrounds/wavy-background

---
title: Wavy Background
description: A cool background effect with waves that move.
---

```vue
<template>
  <WavyBackground class="mx-auto max-w-4xl pb-40">
    <p class="inter-var text-center text-2xl font-bold text-white lg:text-7xl md:text-4xl">
      Hero waves are cool
    </p>
    <p class="inter-var mt-4 text-center text-base font-normal text-white md:text-lg">
      Leverage the power of canvas to create a beautiful hero section
    </p>
  </WavyBackground>
</template>

```

::alert{type="info"}
**Note:** This component requires `simplex-noise` as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="wavy-background" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install simplex-noise
```

```bash [pnpm]
pnpm install simplex-noise
```

```bash [bun]
bun add simplex-noise
```

```bash [yarn]
yarn add simplex-noise
```

::

Copy and paste the following code

```vue
<template>
  <div :class="cn('h-screen flex flex-col items-center justify-center', props.containerClass)">
    <canvas
      id="canvas"
      ref="canvasRef"
      class="absolute z-0"
      :style="{ filter: isSafari ? `blur(${props.blur}px)` : undefined }"
    ></canvas>
    <div :class="cn('relative z-10', props.class)">
      <slot />
    </div>
  </div>
</template>

<script setup lang="ts">
import { createNoise3D } from "simplex-noise";
import { cn } from "@/lib/utils";
import { ref, onMounted, onBeforeUnmount } from "vue";
import { templateRef } from "@vueuse/core";

interface WavyBackgroundProps {
  class?: string;
  containerClass?: string;
  colors?: string[];
  waveWidth?: number;
  backgroundFill?: string;
  blur?: number;
  speed?: "slow" | "fast";
  waveOpacity?: number;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [key: string]: any;
}

const props = withDefaults(defineProps<WavyBackgroundProps>(), {
  colors: () => ["#38bdf8", "#818cf8", "#c084fc", "#e879f9", "#22d3ee"],
  waveWidth: 50,
  backgroundFill: "black",
  blur: 10,
  speed: "fast",
  waveOpacity: 0.5,
});

const noise = createNoise3D();

// Declare variables with null
let w: number,
  h: number,
  nt = 0,
  ctx: CanvasRenderingContext2D | null = null;
let animationId: number;

const canvasRef = templateRef<HTMLCanvasElement | null>("canvasRef");

function getSpeed(): number {
  return props.speed === "slow" ? 0.001 : 0.002;
}

function init() {
  const canvas = canvasRef.value;
  if (canvas) {
    ctx = canvas.getContext("2d");
    if (ctx) {
      const parent = canvasRef.value.parentElement;
      if (parent) {
        w = ctx.canvas.width = parent.clientWidth;
        h = ctx.canvas.height = parent.clientHeight;
      }

      ctx.filter = `blur(${props.blur}px)`;
      window.onresize = () => {
        if (parent) {
          w = ctx!.canvas.width = parent.clientWidth;
          h = ctx!.canvas.height = parent.clientHeight;
        }
        ctx!.filter = `blur(${props.blur}px)`;
      };
      render();
    }
  }
}

function drawWave(n: number) {
  nt += getSpeed();
  for (let i = 0; i < n; i++) {
    ctx!.beginPath();
    ctx!.lineWidth = props.waveWidth!;
    ctx!.strokeStyle = props.colors[i % props.colors!.length];
    for (let x = 0; x < w; x += 5) {
      const y = noise(x / 800, 0.3 * i, nt) * 100;
      ctx!.lineTo(x, y + h * 0.5); // Adjust for height, at 50% of the container
    }
    ctx!.stroke();
    ctx!.closePath();
  }
}

function render() {
  if (ctx) {
    ctx.fillStyle = props.backgroundFill!;
    ctx.globalAlpha = props.waveOpacity!;
    ctx.fillRect(0, 0, w, h);
    drawWave(5);
    animationId = requestAnimationFrame(render);
  }
}

onBeforeUnmount(() => {
  cancelAnimationFrame(animationId);
});

const isSafari = ref(false);
onMounted(() => {
  isSafari.value =
    typeof window !== "undefined" &&
    navigator.userAgent.includes("Safari") &&
    !navigator.userAgent.includes("Chrome");

  init();
});
</script>

```
::

## API

| Prop Name        | Type               | Default                                                   | Description                                                |
| ---------------- | ------------------ | --------------------------------------------------------- | ---------------------------------------------------------- |
| `class`          | `string`           | `-`                                                       | The content to be displayed on top of the wavy background. |
| `containerClass` | `string`           | `-`                                                       | The CSS class to apply to the content container.           |
| `colors`         | `string[]`         | `["#38bdf8", "#818cf8", "#c084fc", "#e879f9", "#22d3ee"]` | The colors of the waves.                                   |
| `waveWidth`      | `number`           | `50`                                                      | The width of the waves.                                    |
| `backgroundFill` | `string`           | `"black"`                                                 | The background color.                                      |
| `blur`           | `number`           | `10`                                                      | The blur effect applied to the waves.                      |
| `speed`          | `"slow" \| "fast"` | `"fast"`                                                  | Range of speed variation for particles.                    |
| `waveOpacity`    | `number`           | `0.5`                                                     | Base radius of particles.                                  |
| `[key: string]`  | `any`              | `-`                                                       | Range of radius variation for particles.                   |

## Features

- **Slot Support**: Easily add any content inside the component using the default slot.

## Credits

- Credits to [Aceternity UI](https://ui.aceternity.com/components/wavy-background).
- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for porting this component.

URL: https://inspira-ui.com/components/buttons/gradient-button

---
title: Gradient Button
description: A stylish animated button with a rotating conic gradient border and customizable properties for a vibrant look.
---

```vue
<template>
  <div class="z-10 flex h-56 w-full flex-col items-center justify-center">
    <GradientButton :bg-color="bgColor">Zooooooooooom 🚀</GradientButton>
  </div>
</template>

<script lang="ts" setup>
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const isDark = computed(() => useColorMode().value == "dark");
const bgColor = computed(() => (isDark.value ? "#000" : "#fff"));
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="gradient-button" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <button
    :class="
      cn(
        'relative flex items-center justify-center min-w-28 min-h-10 overflow-hidden before:absolute before:-inset-[200%] animate-rainbow rainbow-btn',
        props.class,
      )
    "
  >
    <span class="btn-content inline-flex size-full items-center justify-center px-4 py-2">
      <slot />
    </span>
  </button>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { computed } from "vue";

interface GradientButtonProps {
  borderWidth?: number;
  colors?: string[];
  duration?: number;
  borderRadius?: number;
  blur?: number;
  class?: string;
  bgColor?: string;
}

const props = withDefaults(defineProps<GradientButtonProps>(), {
  colors: () => [
    "#FF0000",
    "#FFA500",
    "#FFFF00",
    "#008000",
    "#0000FF",
    "#4B0082",
    "#EE82EE",
    "#FF0000",
  ],
  duration: 2500,
  borderWidth: 2,
  borderRadius: 8,
  blur: 4,
  bgColor: "#000",
});

const durationInMilliseconds = computed(() => `${props.duration}ms`);
const allColors = computed(() => props.colors.join(", "));
const borderWidthInPx = computed(() => `${props.borderWidth}px`);
const borderRadiusInPx = computed(() => `${props.borderRadius}px`);
const blurPx = computed(() => `${props.blur}px`);
</script>

<style scoped>
.animate-rainbow::before {
  content: "";
  background: conic-gradient(v-bind(allColors));
  animation: rotate-rainbow v-bind(durationInMilliseconds) linear infinite;
  filter: blur(v-bind(blurPx));
  padding: v-bind(borderWidthInPx);
}

.rainbow-btn {
  padding: v-bind(borderWidthInPx);
  border-radius: v-bind(borderRadiusInPx);
}

.btn-content {
  border-radius: v-bind(borderRadiusInPx);
  background-color: v-bind(bgColor);
  z-index: 0;
}

@keyframes rotate-rainbow {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
</style>

```

## API

| Prop Name      | Type       | Default              | Description                                                  |
| -------------- | ---------- | -------------------- | ------------------------------------------------------------ |
| `borderWidth`  | `number`   | `2`                  | Width of the gradient border in pixels.                      |
| `colors`       | `string[]` | Rainbow Colors Array | Array of colors used in the conic gradient border.           |
| `duration`     | `number`   | `2500`               | Duration of the gradient rotation animation in milliseconds. |
| `borderRadius` | `number`   | `8`                  | Border radius for rounded corners in pixels.                 |
| `blur`         | `number`   | `4`                  | Blur intensity of the gradient border effect in pixels.      |
| `class`        | `string`   | `""`                 | Additional CSS classes for custom styling.                   |
| `bgColor`      | `string`   | `"#000"`             | Background color of the button content.                      |

## Features

- **Rotating Conic Gradient Border**: A dynamic, rotating conic gradient border creates a visually engaging effect.
- **Customizable Color Palette**: Customize the gradient colors by providing an array of color values.
- **Flexible Styling Options**: Adjust border width, border radius, and blur effect for a tailored look.
- **Slot-Based Content**: Supports a default slot to easily add button content or icons.
- **Smooth Animation Control**: Control the speed of the rotation using the `duration` prop.

URL: https://inspira-ui.com/components/buttons/interactive-hover-button

---
title: Interactive Hover Button
description: A visually engaging button component that responds to hover with dynamic transitions, adapting smoothly between light and dark modes for enhanced user interactivity.
---

```vue
<template>
  <InteractiveHoverButton />
</template>

<script lang="ts" setup></script>

<style></style>

```

## Install using CLI

```vue
<InstallationCli component-id="interactive-hover-button" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <button
    ref="buttonRef"
    :class="
      cn(
        'group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold',
        props.class,
      )
    "
  >
    <div class="flex items-center gap-2">
      <div
        class="size-2 scale-100 rounded-lg bg-primary transition-all duration-300 group-hover:scale-[100.8]"
      ></div>
      <span
        class="inline-block whitespace-nowrap transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0"
      >
        {{ text }}
      </span>
    </div>

    <div
      class="absolute top-0 z-10 flex size-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100"
    >
      <span class="whitespace-nowrap">{{ text }}</span>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        class="lucide lucide-arrow-right"
      >
        <path d="M5 12h14" />
        <path d="m12 5 7 7-7 7" />
      </svg>
    </div>
  </button>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { ref } from "vue";

interface Props {
  text?: string;
  class?: string;
}
const props = withDefaults(defineProps<Props>(), {
  text: "Button",
});

const buttonRef = ref<HTMLButtonElement>();
</script>

<style></style>

```

## API

| Prop Name | Type     | Default  | Description                                    |
| --------- | -------- | -------- | ---------------------------------------------- |
| `text`    | `string` | `Button` | The text to be displayed inside the button.    |
| `class`   | `string` | `""`     | Additional class names to style the component. |

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Inspired and ported from [Magic UI Interactive Hover Button](https://magicui.design/docs/components/interactive-hover-button).

URL: https://inspira-ui.com/components/buttons/rainbow-button

---
title: Rainbow Button
description: A rainbow effect on button.
---

```vue
<template>
  <div class="flex min-h-64 items-center justify-center">
    <RainbowButton> Rainbow Button </RainbowButton>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="rainbow-button" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <component
    :is="is"
    :class="
      cn(
        'rainbow-button',
        'group relative inline-flex h-11 cursor-pointer items-center justify-center rounded-xl border-0 bg-[length:200%] px-8 py-2 font-medium text-primary-foreground transition-colors [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.08*1rem)_solid_transparent] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
        'before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:bg-[length:200%] before:[filter:blur(calc(0.8*1rem))]',
        'bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]',
        'dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]',
        props.class,
      )
    "
  >
    <slot />
  </component>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";
import { computed } from "vue";

interface RainbowButtonProps {
  class?: string;
  is?: string;
  speed?: number;
}

const props = withDefaults(defineProps<RainbowButtonProps>(), {
  speed: 2,
  is: "button",
});

const speedInSeconds = computed(() => `${props.speed}s`);
</script>

<style scoped>
.rainbow-button {
  --color-1: hsl(0 100% 63%);
  --color-2: hsl(270 100% 63%);
  --color-3: hsl(210 100% 63%);
  --color-4: hsl(195 100% 63%);
  --color-5: hsl(90 100% 63%);
  --speed: v-bind(speedInSeconds);
  animation: rainbow var(--speed) infinite linear;
}

.rainbow-button:before {
  animation: rainbow var(--speed) infinite linear;
}

@keyframes rainbow {
  0% {
    background-position: 0;
  }
  100% {
    background-position: 200%;
  }
}
</style>

```

## API

| Prop Name | Type     | Default    | Description                                    |
| --------- | -------- | ---------- | ---------------------------------------------- |
| `class`   | `string` | `""`       | Additional CSS classes to apply to the button. |
| `is`      | `string` | `"button"` | The HTML tag to render for the component.      |
| `speed`   | `number` | `2`        | Duration of the animation in seconds.          |

## Credits

- Credits to [Grzegorz Krol](https://github.com/Grzechu335) for porting this component.
- Credits to [Magic UI](https://magicui.design/docs/components/rainbow-button).

URL: https://inspira-ui.com/components/buttons/ripple-button

---
title: Ripple Button
description: A stylish ripple button component with customizable colors and animation duration.
---

```vue
<template>
  <div class="grid place-content-center p-8">
    <RippleButton> Click me! </RippleButton>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="ripple-button" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <button
    ref="rippleButtonRef"
    :class="
      cn(
        'relative flex cursor-pointer items-center justify-center overflow-hidden',
        'rounded-lg border-2 bg-background px-4 py-2 text-center text-primary',
        $props.class,
      )
    "
    :style="{ '--duration': $props.duration + 'ms' }"
    @click="handleClick"
  >
    <div class="relative z-10">
      <slot />
    </div>

    <span class="pointer-events-none absolute inset-0">
      <span
        v-for="ripple in buttonRipples"
        :key="ripple.key"
        class="ripple-animation absolute rounded-full bg-background opacity-30"
        :style="{
          width: ripple.size + 'px',
          height: ripple.size + 'px',
          top: ripple.y + 'px',
          left: ripple.x + 'px',
          backgroundColor: $props.rippleColor,
          transform: 'scale(0)',
          animationDuration: $props.duration + 'ms',
        }"
      />
    </span>
  </button>
</template>

<script lang="ts" setup>
import { ref, watchEffect, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

interface RippleButtonProps {
  class?: HTMLAttributes["class"];
  rippleColor?: string;
  duration?: number;
}

const props = withDefaults(defineProps<RippleButtonProps>(), {
  rippleColor: "#ADD8E6",
  duration: 600,
});

const emit = defineEmits<{
  (e: "click", event: MouseEvent): void;
}>();

const rippleButtonRef = ref<HTMLButtonElement | null>(null);
const buttonRipples = ref<Array<{ x: number; y: number; size: number; key: number }>>([]);

function handleClick(event: MouseEvent) {
  createRipple(event);
  emit("click", event);
}

function createRipple(event: MouseEvent) {
  const button = rippleButtonRef.value;
  if (!button) return;

  const rect = button.getBoundingClientRect();
  const size = Math.max(rect.width, rect.height);
  const x = event.clientX - rect.left - size / 2;
  const y = event.clientY - rect.top - size / 2;

  const newRipple = { x, y, size, key: Date.now() };
  buttonRipples.value.push(newRipple);
}

watchEffect(() => {
  if (buttonRipples.value.length > 0) {
    const lastRipple = buttonRipples.value[buttonRipples.value.length - 1];
    setTimeout(() => {
      buttonRipples.value = buttonRipples.value.filter((ripple) => ripple.key !== lastRipple.key);
    }, props.duration);
  }
});
</script>

<style scoped>
@keyframes rippling {
  0% {
    opacity: 1;
  }
  100% {
    transform: scale(2);
    opacity: 0;
  }
}

.ripple-animation {
  animation: rippling var(--duration) ease-out;
}
</style>

```

## API

| Prop Name     | Type     | Default     | Description                                       |
| ------------- | -------- | ----------- | ------------------------------------------------- |
| `class`       | `string` | -           | Additional CSS classes for custom styling.        |
| `rippleColor` | `string` | `"#ADD8E6"` | Color of the ripple effect.                       |
| `duration`    | `number` | `600`       | Duration of the ripple animation in milliseconds. |

## Emits

| Event Name | Type    | Description |
| ---------- | ------- | ----------- |
| `click`    | `event` | Click event |

## Credits

- Inspired by [Magic UI's Ripple Button](https://magicui.design/docs/components/ripple-button) component.
- Credits to [kalix127](https://github.com/kalix127) for porting this component.

URL: https://inspira-ui.com/components/buttons/shimmer-button

---
title: Shimmer Button
description: A button with a shimmering animated effect.
---

```vue
<template>
  <div class="z-10 flex min-h-64 items-center justify-center">
    <ShimmerButton
      class="shadow-2xl"
      shimmer-size="2px"
    >
      <span
        class="whitespace-pre-wrap text-center text-sm font-medium leading-none tracking-tight text-white lg:text-lg dark:from-white dark:to-slate-900/10"
      >
        Cool Button
      </span>
    </ShimmerButton>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="shimmer-button" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <button
    :class="
      cn(
        'group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap border border-white/10 px-6 py-3 text-white [background:var(--bg)] [border-radius:var(--radius)] dark:text-black',
        'transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px',
        $props.class,
      )
    "
    :style="{
      '--spread': '90deg',
      '--shimmer-color': shimmerColor,
      '--radius': borderRadius,
      '--speed': shimmerDuration,
      '--cut': shimmerSize,
      '--bg': background,
    }"
  >
    <div :class="cn('-z-30 blur-[2px]', 'absolute inset-0 overflow-visible [container-type:size]')">
      <div
        class="animate-shimmer-btn-shimmer-slide absolute inset-0 h-[100cqh] [aspect-ratio:1] [border-radius:0] [mask:none]"
      >
        <div
          class="animate-shimmer-btn-spin-around absolute -inset-full w-auto rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))] [translate:0_0]"
        />
      </div>
    </div>
    <slot />

    <div
      :class="
        cn(
          'insert-0 absolute size-full',

          'rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]',

          // transition
          'transform-gpu transition-all duration-300 ease-in-out',

          // on hover
          'group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]',

          // on click
          'group-active:shadow-[inset_0_-10px_10px_#ffffff3f]',
        )
      "
    />

    <div
      class="absolute -z-20 [background:var(--bg)] [border-radius:var(--radius)] [inset:var(--cut)]"
    />
  </button>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";

type ShimmerButtonProps = {
  shimmerColor?: string;
  shimmerSize?: string;
  borderRadius?: string;
  shimmerDuration?: string;
  background?: string;
  class?: string;
};

withDefaults(defineProps<ShimmerButtonProps>(), {
  shimmerColor: "#ffffff",
  shimmerSize: "0.05em",
  shimmerDuration: "3s",
  borderRadius: "100px",
  background: "rgba(0, 0, 0, 1)",
});
</script>

<style scoped>
@keyframes shimmer-btn-shimmer-slide {
  to {
    transform: translate(calc(100cqw - 100%), 0);
  }
}

@keyframes shimmer-btn-spin-around {
  0% {
    transform: translateZ(0) rotate(0);
  }
  15%,
  35% {
    transform: translateZ(0) rotate(90deg);
  }
  65%,
  85% {
    transform: translateZ(0) rotate(270deg);
  }
  100% {
    transform: translateZ(0) rotate(360deg);
  }
}

.animate-shimmer-btn-shimmer-slide {
  animation: shimmer-btn-shimmer-slide var(--speed) ease-in-out infinite alternate;
}

.animate-shimmer-btn-spin-around {
  animation: shimmer-btn-spin-around calc(var(--speed) * 2) infinite linear;
}
</style>

```

## API

| Prop Name         | Type     | Default              | Description                                             |
| ----------------- | -------- | -------------------- | ------------------------------------------------------- |
| `class`           | `string` | `""`                 | Additional CSS classes to apply to the button.          |
| `shimmerColor`    | `string` | `"#ffffff"`          | Color of the shimmer effect.                            |
| `shimmerSize`     | `string` | `"0.05em"`           | Size of the shimmer effect.                             |
| `borderRadius`    | `string` | `"100px"`            | Border radius of the button.                            |
| `shimmerDuration` | `string` | `"3s"`               | Duration of the shimmer animation.                      |
| `background`      | `string` | `"rgba(0, 0, 0, 1)"` | Background color of the button. Can be rgb or hex code. |

## Features

- **Shimmering Effect**: Displays a continuous shimmering animation on the button.
- **Customizable Appearance**: Adjust shimmer color, size, duration, border radius, and background color.
- **Slot Support**: Easily add any content inside the button using the default slot.
- **Interactive States**: Includes hover and active states for enhanced user interaction.
- **Responsive Design**: Adapts to different screen sizes and resolutions seamlessly.

## Credits

- Ported from [Magic UI Shimmer Button](https://magicui.design/docs/components/shimmer-button).

URL: https://inspira-ui.com/components/cards/3d-card

---
title: 3D Card Effect
description: A card perspective effect, hover over the card to elevate card elements.
---

```vue
<template>
  <ClientOnly>
    <CardContainer>
      <CardBody
        class="group/card relative size-auto rounded-xl border border-black/[0.1] bg-gray-50 p-6 sm:w-[30rem] dark:border-white/[0.2] dark:bg-black dark:hover:shadow-2xl dark:hover:shadow-emerald-500/[0.1]"
      >
        <CardItem
          :translate-z="50"
          class="text-xl font-bold text-neutral-600 dark:text-white"
        >
          Make things float in air
        </CardItem>
        <CardItem
          as="p"
          translate-z="60"
          class="mt-2 max-w-sm text-sm text-neutral-500 dark:text-neutral-300"
        >
          Hover over this card to unleash the power of CSS perspective
        </CardItem>
        <CardItem
          :translate-z="100"
          class="mt-4 w-full"
        >
          <img
            src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
            height="1000"
            width="1000"
            class="h-60 w-full rounded-xl object-cover group-hover/card:shadow-xl"
            alt="thumbnail"
          />
        </CardItem>
        <div class="mt-20 flex items-center justify-between">
          <CardItem
            :translate-z="20"
            as="a"
            href="https://rahulv.dev"
            target="__blank"
            class="rounded-xl px-4 py-2 text-xs font-normal dark:text-white"
          >
            Visit →
          </CardItem>
          <CardItem
            :translate-z="20"
            as="button"
            class="rounded-xl bg-black px-4 py-2 text-xs font-bold text-white dark:bg-white dark:text-black"
          >
            Get Started
          </CardItem>
        </div>
      </CardBody>
    </CardContainer>
  </ClientOnly>
</template>

<script setup lang="ts">
import { CardContainer, CardBody, CardItem } from "~/components/content/inspira/ui/card-3d";
</script>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="card-3d" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{filename="CardContainer.vue" language="vue" componentName="CardContainer" type="ui" id="card-3d"}
:CodeViewerTab{filename="CardBody.vue" language="vue" componentName="CardBody" type="ui" id="card-3d"}
:CodeViewerTab{filename="CardItem.vue" language="vue" componentName="CardItem" type="ui" id="card-3d"}

```ts [useMouseState.ts]
import { ref, readonly } from "vue";

export function useMouseState() {
  const isMouseEntered = ref(false);

  function setMouseEntered(value: boolean) {
    isMouseEntered.value = value;
  }

  return {
    isMouseEntered: readonly(isMouseEntered),
    setMouseEntered,
  };
}
```

::

## Examples

With rotation

```vue
<template>
  <ClientOnly>
    <CardContainer>
      <CardBody
        class="group/card relative size-auto rounded-xl border border-black/[0.1] bg-gray-50 p-6 sm:w-[30rem] dark:border-white/[0.2] dark:bg-black dark:hover:shadow-2xl dark:hover:shadow-emerald-500/[0.1]"
      >
        <CardItem
          :translate-z="50"
          class="text-xl font-bold text-neutral-600 dark:text-white"
        >
          Make things float in air
        </CardItem>
        <CardItem
          as="p"
          translate-z="60"
          class="mt-2 max-w-sm text-sm text-neutral-500 dark:text-neutral-300"
        >
          Hover over this card to unleash the power of CSS perspective
        </CardItem>
        <CardItem
          :translate-z="100"
          :rotate-x="20"
          :rotate-z="-10"
          class="mt-4 w-full"
        >
          <img
            src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
            height="1000"
            width="1000"
            class="h-60 w-full rounded-xl object-cover group-hover/card:shadow-xl"
            alt="thumbnail"
          />
        </CardItem>
        <div class="mt-20 flex items-center justify-between">
          <CardItem
            :translate-z="20"
            :translate-x="-40"
            as="a"
            href="https://rahulv.dev"
            target="__blank"
            class="rounded-xl px-4 py-2 text-xs font-normal dark:text-white"
          >
            Visit →
          </CardItem>
          <CardItem
            :translate-z="20"
            :translate-x="40"
            as="button"
            class="rounded-xl bg-black px-4 py-2 text-xs font-bold text-white dark:bg-white dark:text-black"
          >
            Get Started
          </CardItem>
        </div>
      </CardBody>
    </CardContainer>
  </ClientOnly>
</template>

<script setup lang="ts">
import { CardContainer, CardBody, CardItem } from "~/components/content/inspira/ui/card-3d";
</script>

```

## API

### `CardContainer`

The `CardContainer` component serves as a wrapper for the 3D card effect. It manages mouse events to create a 3D perspective.

#### Props

| Prop Name        | Type   | Default | Description                                                 |
| ---------------- | ------ | ------- | ----------------------------------------------------------- |
| `class`          | String | `null`  | Additional classes for styling the inner container element. |
| `containerClass` | String | `null`  | Additional classes for styling the outer container element. |

#### Usage

```vue [MyCardComponent.vue]
<CardContainer containerClass="additional-class">
  <!-- Your content here -->
</CardContainer>
```

### `CardBody`

The `CardBody` component is a flexible container with preserved 3D styling. It is intended to be used within a `CardContainer` to hold content with a 3D transformation effect.

#### Props

| Prop Name | Type   | Default | Description                            |
| --------- | ------ | ------- | -------------------------------------- |
| `class`   | String | `null`  | Additional classes for custom styling. |

#### Usage

```vue [MyCardComponent.vue]
<CardBody class="custom-class">
  <!-- Your content here -->
</CardBody>
```

### `CardItem`

The `CardItem` component represents individual items within the 3D card. It supports various transformations (translation and rotation) to create dynamic 3D effects.

#### Props

| Prop Name    | Type          | Default | Description                                                     |
| ------------ | ------------- | ------- | --------------------------------------------------------------- |
| `as`         | String        | `"div"` | The HTML tag or component to use for the item.                  |
| `class`      | String        | `null`  | Additional classes for styling the item.                        |
| `translateX` | Number/String | `0`     | X-axis translation in pixels.                                   |
| `translateY` | Number/String | `0`     | Y-axis translation in pixels.                                   |
| `translateZ` | Number/String | `0`     | Z-axis translation in pixels, used to control the depth effect. |
| `rotateX`    | Number/String | `0`     | X-axis rotation in degrees.                                     |
| `rotateY`    | Number/String | `0`     | Y-axis rotation in degrees.                                     |
| `rotateZ`    | Number/String | `0`     | Z-axis rotation in degrees.                                     |

#### Usage

```vue [MyCardComponent.vue]
<CardItem as="p" translateZ="50" class="custom-item-class">
  Your text or content here
</CardItem>
```

URL: https://inspira-ui.com/components/cards/card-spotlight

---
title: Card Spotlight
description: A card component with a dynamic spotlight effect that follows the mouse cursor, enhancing visual interactivity.
---

```vue
<template>
  <div class="flex h-[500px] w-full flex-col gap-4 lg:h-[250px] lg:flex-row">
    <CardSpotlight
      class="cursor-pointer flex-col items-center justify-center whitespace-nowrap text-4xl shadow-2xl"
      :gradient-color="isDark ? '#363636' : '#C9C9C9'"
    >
      Card Spotlight
    </CardSpotlight>
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const isDark = computed(() => useColorMode().value == "dark");
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="card-spotlight" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    :class="[
      'group relative flex size-full overflow-hidden rounded-xl border bg-neutral-100 text-black dark:bg-neutral-900 dark:text-white',
      $props.class,
    ]"
    @mousemove="handleMouseMove"
    @mouseleave="handleMouseLeave"
  >
    <div :class="cn('relative z-10', props.slotClass)">
      <slot></slot>
    </div>
    <div
      class="pointer-events-none absolute inset-0 rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100"
      :style="{
        background: backgroundStyle,
        opacity: gradientOpacity,
      }"
    ></div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = withDefaults(
  defineProps<{
    class: HTMLAttributes["class"];
    slotClass: HTMLAttributes["class"];
    gradientSize: number;
    gradientColor: string;
    gradientOpacity: number;
  }>(),
  {
    class: "",
    slotClass: "",
    gradientSize: 200,
    gradientColor: "#262626",
    gradientOpacity: 0.8,
  },
);

const mouseX = ref(-props.gradientSize * 10);
const mouseY = ref(-props.gradientSize * 10);

function handleMouseMove(e: MouseEvent) {
  const target = e.currentTarget as HTMLElement;
  const rect = target.getBoundingClientRect();
  mouseX.value = e.clientX - rect.left;
  mouseY.value = e.clientY - rect.top;
}

function handleMouseLeave() {
  mouseX.value = -props.gradientSize * 10;
  mouseY.value = -props.gradientSize * 10;
}

onMounted(() => {
  mouseX.value = -props.gradientSize * 10;
  mouseY.value = -props.gradientSize * 10;
});

const backgroundStyle = computed(() => {
  return `radial-gradient(
    circle at ${mouseX.value}px ${mouseY.value}px,
    ${props.gradientColor} 0%,
    rgba(0, 0, 0, 0) 70%
  )`;
});
</script>

```

## API

| Prop Name         | Type     | Default     | Description                                                 |
| ----------------- | -------- | ----------- | ----------------------------------------------------------- |
| `gradientSize`    | `number` | `200`       | Radius in pixels of the spotlight effect.                   |
| `gradientColor`   | `string` | `'#262626'` | The color of the spotlight gradient.                        |
| `gradientOpacity` | `number` | `0.8`       | The opacity level of the spotlight gradient effect.         |
| `slotClass`       | `string` | `undefined` | Class to apply to the parent container containing the slot. |

## Features

- **Interactive Spotlight Effect**: Displays a dynamic spotlight that follows the user's mouse cursor, enhancing user engagement and visual appeal.

- **Customizable Appearance**: Easily adjust the `gradientSize`, `gradientColor`, and `gradientOpacity` props to tailor the spotlight effect to your design preferences.

- **Easy Integration**: Wrap any content within the `<CardSpotlight>` component to instantly add the spotlight effect without additional configuration.

- **Responsive Design**: The component adapts smoothly to different container sizes, ensuring a consistent experience across various devices and screen sizes.

- **Performance Optimized**: Utilizes Vue's reactivity for efficient updates, ensuring smooth animations without compromising application performance.

## Credits

- Inspired by Magic Card component from [Magic UI](https://magicui.design/docs/components/magic-card).

URL: https://inspira-ui.com/components/cards/direction-aware-hover

---
title: Direction Aware Hover
description: A direction aware hover card, that displays an image with dynamic hover effects and customizable content overlay.
---

```vue
<template>
  <div class="container mx-auto p-8">
    <h1 class="mb-8 text-3xl font-bold">Direction Aware Hover Card Examples</h1>

    <div class="grid grid-cols-1 gap-8 lg:grid-cols-3 md:grid-cols-2">
      <!-- Basic usage -->
      <DirectionAwareHover
        image-url="https://images.unsplash.com/photo-1728755833852-2c138c84cfb1?q=80&w=2672&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
        class="shadow-lg"
      >
        <h2 class="text-xl font-semibold">Beautiful Landscape</h2>
        <p class="mt-2">Discover nature's wonders</p>
      </DirectionAwareHover>

      <!-- Custom styling -->
      <DirectionAwareHover
        image-url="https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80"
        class="border-4 border-primary"
        image-class="scale-100 hover:scale-110"
        children-class="bg-black bg-opacity-50 p-4 rounded"
      >
        <h2 class="text-xl font-semibold">Urban Adventure</h2>
        <p class="mt-2">Explore the city lights</p>
      </DirectionAwareHover>

      <!-- With button -->
      <DirectionAwareHover
        image-url="https://images.unsplash.com/photo-1664710476481-1213c456c56c?q=80&w=2672&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
        class="overflow-hidden rounded-xl shadow-xl"
      >
        <h2 class="text-xl font-semibold">Culinary Delights</h2>
        <p class="mt-2">Savor exquisite flavors</p>
        <button class="mt-4 rounded bg-white px-4 py-2 text-black">View Recipe</button>
      </DirectionAwareHover>
    </div>
  </div>
</template>

<script setup lang="ts"></script>

```

## Install using CLI

```vue
<InstallationCli component-id="direction-aware-hover" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="divRef"
    :class="containerClass"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
  >
    <div class="relative size-full overflow-hidden">
      <transition name="fade">
        <div
          v-show="direction !== null"
          :class="overlayClass"
        />
      </transition>
      <div
        class="relative size-full bg-gray-50 transition-transform duration-300 dark:bg-black"
        :class="imageContainerClass"
      >
        <img
          :src="imageUrl"
          alt="image"
          :class="imageClass"
          width="1000"
          height="1000"
        />
      </div>
      <transition name="fade">
        <div
          v-show="direction !== null"
          :class="childrenClass"
        >
          <slot />
        </div>
      </transition>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
import { cn } from "@/lib/utils";

interface Props {
  imageUrl: string;
  childrenClass?: string;
  imageClass?: string;
  class?: string;
}

const props = withDefaults(defineProps<Props>(), {
  childrenClass: undefined,
  imageClass: undefined,
  class: undefined,
});

const divRef = ref<HTMLDivElement | null>(null);
const direction = ref<"top" | "bottom" | "left" | "right" | null>(null);

function handleMouseEnter(event: MouseEvent) {
  if (!divRef.value) return;

  const fetchedDirection = getDirection(event, divRef.value);
  switch (fetchedDirection) {
    case 0:
      direction.value = "top";
      break;
    case 1:
      direction.value = "right";
      break;
    case 2:
      direction.value = "bottom";
      break;
    case 3:
      direction.value = "left";
      break;
    default:
      direction.value = "left";
      break;
  }
}

function handleMouseLeave() {
  direction.value = null;
}

function getDirection(ev: MouseEvent, obj: HTMLElement) {
  const { width: w, height: h, left, top } = obj.getBoundingClientRect();
  const x = ev.clientX - left - (w / 2) * (w > h ? h / w : 1);
  const y = ev.clientY - top - (h / 2) * (h > w ? w / h : 1);
  const d = Math.round(Math.atan2(y, x) / 1.57079633 + 5) % 4;
  return d;
}

const containerClass = computed(() =>
  cn(
    "group/card relative h-60 w-60 overflow-hidden rounded-lg bg-transparent md:h-96 md:w-96",
    props.class,
  ),
);

const imageClass = computed(() =>
  cn("h-full w-full scale-150 object-cover transition-transform duration-300", props.imageClass),
);

const childrenClass = computed(() =>
  cn(
    "absolute bottom-4 left-4 z-40 text-white transition-opacity duration-300",
    props.childrenClass,
  ),
);

const overlayClass = computed(
  () =>
    `absolute inset-0 z-10 bg-black/40 transition-opacity duration-300 ${
      direction.value === "top"
        ? "-translate-y-full"
        : direction.value === "bottom"
          ? "translate-y-full"
          : direction.value === "left"
            ? "-translate-x-full"
            : direction.value === "right"
              ? "translate-x-full"
              : ""
    }`,
);

const imageContainerClass = computed(() => ({
  "translate-y-5": direction.value === "top",
  "-translate-y-5": direction.value === "bottom",
  "translate-x-5": direction.value === "left",
  "-translate-x-5": direction.value === "right",
}));
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

```

## API

| Prop Name       | Type     | Default     | Description                                     |
| --------------- | -------- | ----------- | ----------------------------------------------- |
| `imageUrl`      | `string` | Required    | The URL of the image to be displayed.           |
| `class`         | `string` | `undefined` | Additional CSS classes for the card container.  |
| `imageClass`    | `string` | `undefined` | Additional CSS classes for the image element.   |
| `childrenClass` | `string` | `undefined` | Additional CSS classes for the content overlay. |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for porting this component.

- Ported from [Aceternity UI's Direction Aware Hover](https://ui.aceternity.com/components/direction-aware-hover)

URL: https://inspira-ui.com/components/cards/flip-card

---
title: Flip Card
description: A dynamic flip card with smooth 180-degree flipping animations along both the X and Y axes, providing an engaging and interactive visual effect.
---

```vue
<template>
  <div class="flex items-center justify-center">
    <FlipCard>
      <template #default>
        <img
          src="https://images.unsplash.com/photo-1525373698358-041e3a460346?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3"
          alt="image"
          class="size-full rounded-2xl object-cover shadow-2xl shadow-black/40"
        />
        <div class="absolute bottom-4 left-4 text-xl font-bold text-white">Inspira UI</div>
      </template>
      <template #back>
        <div class="flex min-h-full flex-col gap-2">
          <h1 class="text-xl font-bold text-white">Inspira UI</h1>
          <p
            class="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100"
          >
            Inspira UI offers beautifully designed, reusable animation components and includes
            custom components developed by us and contributed by the community.
          </p>
        </div>
      </template>
    </FlipCard>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="flip-card" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div :class="cn('group h-72 w-56 [perspective:1000px]', props.class)">
    <div
      :class="
        cn(
          'relative h-full rounded-2xl transition-all duration-500 [transform-style:preserve-3d]',
          rotation[0],
        )
      "
    >
      <!-- Front -->
      <div
        class="absolute size-full overflow-hidden rounded-2xl border [backface-visibility:hidden]"
      >
        <slot />
      </div>

      <!-- Back -->
      <div
        :class="
          cn(
            'absolute h-full w-full overflow-hidden rounded-2xl border bg-black/80 p-4 text-slate-200 [backface-visibility:hidden]',
            rotation[1],
          )
        "
      >
        <slot name="back" />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";
import { computed } from "vue";

interface FlipCardProps {
  rotate?: "x" | "y";
  class?: string;
}

const props = withDefaults(defineProps<FlipCardProps>(), {
  rotate: "y",
});
const rotationClass = {
  x: ["group-hover:[transform:rotateX(180deg)]", "[transform:rotateX(180deg)]"],
  y: ["group-hover:[transform:rotateY(180deg)]", "[transform:rotateY(180deg)]"],
};

const rotation = computed(() => rotationClass[props.rotate]);
</script>

```

## Examples

### Horizontal Flip

```vue
<template>
  <div class="flex items-center justify-center">
    <FlipCard rotate="x">
      <template #default>
        <img
          src="https://images.unsplash.com/photo-1525373698358-041e3a460346?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3"
          alt="image"
          class="size-full rounded-2xl object-cover shadow-2xl shadow-black/40"
        />
      </template>
      <template #back>
        <div class="flex min-h-full flex-col gap-2">
          <h1 class="text-xl font-bold text-white">Inspira UI</h1>
          <p
            class="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100"
          >
            Inspira UI offers beautifully designed, reusable animation components and includes
            custom components developed by us and contributed by the community.
          </p>
        </div>
      </template>
    </FlipCard>
  </div>
</template>

```

## API

| Prop Name | Type     | Default | Description                                |
| --------- | -------- | ------- | ------------------------------------------ |
| `class`   | `string` | `-`     | The class to be applied to the component.  |
| `rotate`  | `x \| y` | `y`     | You can pass the rotate value as you want. |

| Slot Name | Description                 |
| --------- | --------------------------- |
| `default` | Component to show as front. |
| `back`    | Component to show as back.  |

## Credits

- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for this component.

URL: https://inspira-ui.com/components/cards/glare-card

---
title: Glare Card
description: A glare effect that happens on hover, as seen on Linear's website.
---

```vue
<template>
  <div class="flex items-center justify-center">
    <GlareCard class="flex flex-col items-center justify-center">
      <img src="/logo-dark.svg" />
      <p class="mt-4 text-xl font-bold text-white">Inspira UI</p>
    </GlareCard>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="glare-card" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="refElement"
    class="container-style duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] container relative isolate w-[320px] transition-transform will-change-transform [aspect-ratio:17/21] [contain:layout_style] [perspective:600px]"
    @pointermove="handlePointerMove"
    @pointerenter="handlePointerEnter"
    @pointerleave="handlePointerLeave"
  >
    <div
      class="duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] grid h-full origin-center overflow-hidden rounded-lg border border-slate-800 transition-transform will-change-transform [transform:rotateY(var(--r-x))_rotateX(var(--r-y))] hover:filter-none hover:[--duration:200ms] hover:[--easing:linear] hover:[--opacity:0.6]"
    >
      <div
        class="grid size-full mix-blend-soft-light [clip-path:inset(0_0_0_0_round_var(--radius))] [grid-area:1/1]"
      >
        <div :class="cn('size-full bg-slate-950', props.class)">
          <slot />
        </div>
      </div>
      <div
        class="transition-background duration-[var(--duration)] ease-[var(--easing)] delay-[var(--delay)] will-change-background grid size-full opacity-[var(--opacity)] mix-blend-soft-light transition-opacity [background:radial-gradient(farthest-corner_circle_at_var(--m-x)_var(--m-y),_rgba(255,255,255,0.8)_10%,_rgba(255,255,255,0.65)_20%,_rgba(255,255,255,0)_90%)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1]"
      />
      <div
        class="background-style will-change-background after:grid-area-[inherit] after:bg-repeat-[inherit] after:bg-attachment-[inherit] after:bg-origin-[inherit] after:bg-clip-[inherit] relative grid size-full opacity-[var(--opacity)] mix-blend-color-dodge transition-opacity [background-blend-mode:hue_hue_hue_overlay] [background:var(--pattern),_var(--rainbow),_var(--diagonal),_var(--shade)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1] after:bg-[inherit] after:mix-blend-exclusion after:content-[\'\'] after:[background-blend-mode:soft-light,_hue,_hard-light] after:[background-position:center,_0%_var(--bg-y),_calc(var(--bg-x)*_-1)_calc(var(--bg-y)*_-1),_var(--bg-x)_var(--bg-y)] after:[background-size:var(--foil-size),_200%_400%,_800%,_200%]"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";
import { useTimeoutFn } from "@vueuse/core";
import { ref } from "vue";

interface GlareCardProps {
  class?: string;
}

const props = defineProps<GlareCardProps>();

const isPointerInside = ref(false);
const refElement = ref<HTMLElement | null>(null);

const state = ref({
  glare: { x: 50, y: 50 },
  background: { x: 50, y: 50 },
  rotate: { x: 0, y: 0 },
});

function handlePointerMove(event: PointerEvent) {
  const rotateFactor = 0.4;
  const rect = refElement.value?.getBoundingClientRect();
  if (rect) {
    const position = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    };
    const percentage = {
      x: (100 / rect.width) * position.x,
      y: (100 / rect.height) * position.y,
    };
    const delta = {
      x: percentage.x - 50,
      y: percentage.y - 50,
    };
    state.value.background.x = 50 + percentage.x / 4 - 12.5;
    state.value.background.y = 50 + percentage.y / 3 - 16.67;
    state.value.rotate.x = -(delta.x / 3.5) * rotateFactor;
    state.value.rotate.y = (delta.y / 2) * rotateFactor;
    state.value.glare.x = percentage.x;
    state.value.glare.y = percentage.y;
  }
}

function handlePointerEnter() {
  isPointerInside.value = true;
  useTimeoutFn(() => {
    if (isPointerInside.value && refElement.value) {
      refElement.value.style.setProperty("--duration", "0s");
    }
  }, 300);
}

function handlePointerLeave() {
  isPointerInside.value = false;
  if (refElement.value) {
    refElement.value.style.removeProperty("--duration");
    state.value.rotate = { x: 0, y: 0 };
  }
}
</script>

<style scoped>
.background-style {
  --step: 5%;
  --foil-svg: url("data:image/svg+xml,%3Csvg width='26' height='26' viewBox='0 0 26 26' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.99994 3.419C2.99994 3.419 21.6142 7.43646 22.7921 12.153C23.97 16.8695 3.41838 23.0306 3.41838 23.0306' stroke='white' stroke-width='5' stroke-miterlimit='3.86874' stroke-linecap='round' style='mix-blend-mode:darken'/%3E%3C/svg%3E");
  --pattern: var(--foil-svg) center/100% no-repeat;
  --rainbow: repeating-linear-gradient(
      0deg,
      rgb(255, 119, 115) calc(var(--step) * 1),
      rgba(255, 237, 95, 1) calc(var(--step) * 2),
      rgba(168, 255, 95, 1) calc(var(--step) * 3),
      rgba(131, 255, 247, 1) calc(var(--step) * 4),
      rgba(120, 148, 255, 1) calc(var(--step) * 5),
      rgb(216, 117, 255) calc(var(--step) * 6),
      rgb(255, 119, 115) calc(var(--step) * 7)
    )
    0% var(--bg-y) / 200% 700% no-repeat;
  --diagonal: repeating-linear-gradient(
      128deg,
      #0e152e 0%,
      hsl(180, 10%, 60%) 3.8%,
      hsl(180, 10%, 60%) 4.5%,
      hsl(180, 10%, 60%) 5.2%,
      #0e152e 10%,
      #0e152e 12%
    )
    var(--bg-x) var(--bg-y) / 300% no-repeat;
  --shade: radial-gradient(
      farthest-corner circle at var(--m-x) var(--m-y),
      rgba(255, 255, 255, 0.1) 12%,
      rgba(255, 255, 255, 0.15) 20%,
      rgba(255, 255, 255, 0.25) 120%
    )
    var(--bg-x) var(--bg-y) / 300% no-repeat;
  background-blend-mode: hue, hue, hue, overlay;
}
.container-style {
  --m-x: v-bind(state.glare.x + "%");
  --m-y: v-bind(state.glare.y + "%");
  --r-x: v-bind(state.rotate.x + "deg");
  --r-y: v-bind(state.rotate.y + "deg");
  --bg-x: v-bind(state.background.x + "%");
  --bg-y: v-bind(state.background.y + "%");
  --duration: 300ms;
  --foil-size: 100%;
  --opacity: 0;
  --radius: 48px;
  --easing: ease;
  --transition: var(--duration) var(--easing);
}
</style>

```

## Examples

### Multiple Cards

```vue
<template>
  <div class="grid grid-cols-1 gap-10 overflow-scroll md:grid-cols-3">
    <GlareCard class="flex flex-col items-center justify-center">
      <img src="/logo-dark.svg" />
    </GlareCard>
    <GlareCard class="flex flex-col items-center justify-center">
      <img
        class="absolute inset-0 size-full object-cover"
        src="https://images.unsplash.com/photo-1512618831669-521d4b375f5d?q=80&w=3388&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
      />
    </GlareCard>
    <GlareCard class="flex flex-col items-start justify-end px-6 py-8">
      <p class="text-lg font-bold text-white">The greatest trick</p>
      <p class="mt-4 text-base font-normal text-neutral-200">
        The greatest trick the devil ever pulled was to convince the world that he didn&apos;t
        exist.
      </p>
    </GlareCard>
  </div>
</template>

```

## API

| Prop Name | Type     | Default | Description                                               |
| --------- | -------- | ------- | --------------------------------------------------------- |
| `class`   | `string` | `-`     | Additional Tailwind CSS class names to apply to the card. |

## Features

- **Slot Support**: Easily add any content inside the component using the default slot.

## Credits

- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for this component.
- Inspired from [Aceternity UI](https://ui.aceternity.com/components/glare-card).

URL: https://inspira-ui.com/components/device-mocks/iphone-mockup

---
title: iPhone Mockup
description: An SVG mockup of an iPhone.
---

```vue
<!-- eslint-disable check-file/filename-naming-convention -->
<template>
  <div class="relative flex w-full flex-col items-center justify-center p-8">
    <iPhone15ProMockup
      src="/images/inspira-ss-phone.png"
      class="size-full max-w-sm"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="iphone-mockup" />
```

## Install Manually

Copy and paste the following code

```vue
<!-- eslint-disable check-file/filename-naming-convention -->
<template>
  <svg
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
    :width="width"
    :height="height"
    :viewBox="`0 0 ${width} ${height}`"
  >
    <path
      d="M2 73C2 32.6832 34.6832 0 75 0H357C397.317 0 430 32.6832 430 73V809C430 849.317 397.317 882 357 882H75C34.6832 882 2 849.317 2 809V73Z"
      class="fill-[#E5E5E5] dark:fill-[#404040]"
    ></path>
    <path
      d="M0 171C0 170.448 0.447715 170 1 170H3V204H1C0.447715 204 0 203.552 0 203V171Z"
      class="fill-[#E5E5E5] dark:fill-[#404040]"
    ></path>
    <path
      d="M1 234C1 233.448 1.44772 233 2 233H3.5V300H2C1.44772 300 1 299.552 1 299V234Z"
      class="fill-[#E5E5E5] dark:fill-[#404040]"
    ></path>
    <path
      d="M1 319C1 318.448 1.44772 318 2 318H3.5V385H2C1.44772 385 1 384.552 1 384V319Z"
      class="fill-[#E5E5E5] dark:fill-[#404040]"
    ></path>
    <path
      d="M430 279H432C432.552 279 433 279.448 433 280V384C433 384.552 432.552 385 432 385H430V279Z"
      class="fill-[#E5E5E5] dark:fill-[#404040]"
    ></path>
    <path
      d="M6 74C6 35.3401 37.3401 4 76 4H356C394.66 4 426 35.3401 426 74V808C426 846.66 394.66 878 356 878H76C37.3401 878 6 846.66 6 808V74Z"
      class="fill-white dark:fill-[#262626]"
    ></path>
    <path
      opacity="0.5"
      d="M174 5H258V5.5C258 6.60457 257.105 7.5 256 7.5H176C174.895 7.5 174 6.60457 174 5.5V5Z"
      class="fill-[#E5E5E5] dark:fill-[#404040]"
    ></path>
    <path
      d="M21.25 75C21.25 44.2101 46.2101 19.25 77 19.25H355C385.79 19.25 410.75 44.2101 410.75 75V807C410.75 837.79 385.79 862.75 355 862.75H77C46.2101 862.75 21.25 837.79 21.25 807V75Z"
      class="fill-[#E5E5E5] stroke-[#E5E5E5] stroke-[0.5] dark:fill-[#404040] dark:stroke-[#404040]"
    ></path>
    <image
      v-if="src"
      x="21.25"
      y="19.25"
      width="389.5"
      height="843.5"
      preserveAspectRatio="xMidYMid slice"
      style="clip-path: url(#roundedCorners)"
      :href="src"
    ></image>

    <path
      d="M154 48.5C154 38.2827 162.283 30 172.5 30H259.5C269.717 30 278 38.2827 278 48.5C278 58.7173 269.717 67 259.5 67H172.5C162.283 67 154 58.7173 154 48.5Z"
      class="fill-[#F5F5F5] dark:fill-[#262626]"
    ></path>
    <path
      d="M249 48.5C249 42.701 253.701 38 259.5 38C265.299 38 270 42.701 270 48.5C270 54.299 265.299 59 259.5 59C253.701 59 249 54.299 249 48.5Z"
      class="fill-[#F5F5F5] dark:fill-[#262626]"
    ></path>
    <path
      d="M254 48.5C254 45.4624 256.462 43 259.5 43C262.538 43 265 45.4624 265 48.5C265 51.5376 262.538 54 259.5 54C256.462 54 254 51.5376 254 48.5Z"
      class="fill-[#E5E5E5] dark:fill-[#404040]"
    ></path>
    <defs>
      <clipPath id="roundedCorners">
        <rect
          x="21.25"
          y="19.25"
          width="389.5"
          height="843.5"
          rx="55.75"
          ry="55.75"
        ></rect>
      </clipPath>
    </defs>
  </svg>
</template>

<script lang="ts" setup>
// eslint-disable-next-line check-file/filename-naming-convention
interface Props {
  width?: number;
  height?: number;
  src?: string;
}

withDefaults(defineProps<Props>(), {
  width: 433,
  height: 882,
});
</script>

```

## API

| Prop Name | Type     | Default | Description                                    |
| --------- | -------- | ------- | ---------------------------------------------- |
| `width`   | `number` | `433`   | Width of the mockup SVG in pixels.             |
| `height`  | `number` | `882`   | Height of the mockup SVG in pixels.            |
| `src`     | `string` | `null`  | URL of the image to display inside the mockup. |

## Features

- **Realistic iPhone 15 Pro Mockup**: Provides an accurate SVG representation of the iPhone 15 Pro, perfect for showcasing mobile app designs or website previews.
- **Customizable Dimensions**: Adjust the `width` and `height` props to fit your specific design requirements.
- **Image Display Support**: Use the `src` prop to insert any image into the mockup screen area, allowing for dynamic content display.
- **Light and Dark Mode Compatibility**: The mockup adapts its colors based on the theme, ensuring consistency in both light and dark modes.
- **Easy Integration**: Simple to include in your Vue projects with minimal setup.

## Credits

- Ported from [Magic UI](https://magicui.design/docs/components/iphone-15-pro).

URL: https://inspira-ui.com/components/device-mocks/safari-mockup

---
title: Safari Mockup
description: An SVG mockup of the Safari browser.
---

```vue
<template>
  <div class="relative p-4">
    <SafariMockup
      url="inspira-ui.com"
      src="/images/inspira-ss.png"
      class="size-full"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="safari-mockup" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <svg
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
    :width="width"
    :height="height"
    :viewBox="`0 0 ${width} ${height}`"
  >
    <g clipPath="url(#path0)">
      <path
        d="M0 52H1202V741C1202 747.627 1196.63 753 1190 753H12C5.37258 753 0 747.627 0 741V52Z"
        class="fill-[#E5E5E5] dark:fill-[#404040]"
      ></path>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M0 12C0 5.37258 5.37258 0 12 0H1190C1196.63 0 1202 5.37258 1202 12V52H0L0 12Z"
        class="fill-[#E5E5E5] dark:fill-[#404040]"
      ></path>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M1.06738 12C1.06738 5.92487 5.99225 1 12.0674 1H1189.93C1196.01 1 1200.93 5.92487 1200.93 12V51H1.06738V12Z"
        class="fill-white dark:fill-[#262626]"
      ></path>
      <circle
        cx="27"
        cy="25"
        r="6"
        class="fill-[#E5E5E5] dark:fill-[#404040]"
      ></circle>
      <circle
        cx="47"
        cy="25"
        r="6"
        class="fill-[#E5E5E5] dark:fill-[#404040]"
      ></circle>
      <circle
        cx="67"
        cy="25"
        r="6"
        class="fill-[#E5E5E5] dark:fill-[#404040]"
      ></circle>
      <path
        d="M286 17C286 13.6863 288.686 11 292 11H946C949.314 11 952 13.6863 952 17V35C952 38.3137 949.314 41 946 41H292C288.686 41 286 38.3137 286 35V17Z"
        class="fill-[#E5E5E5] dark:fill-[#404040]"
      ></path>
      <g class="mix-blend-luminosity">
        <path
          d="M566.269 32.0852H572.426C573.277 32.0852 573.696 31.6663 573.696 30.7395V25.9851C573.696 25.1472 573.353 24.7219 572.642 24.6521V23.0842C572.642 20.6721 571.036 19.5105 569.348 19.5105C567.659 19.5105 566.053 20.6721 566.053 23.0842V24.6711C565.393 24.7727 565 25.1917 565 25.9851V30.7395C565 31.6663 565.418 32.0852 566.269 32.0852ZM567.272 22.97C567.272 21.491 568.211 20.6785 569.348 20.6785C570.478 20.6785 571.423 21.491 571.423 22.97V24.6394L567.272 24.6458V22.97Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <text
          x="580"
          y="30"
          fill="#A3A3A3"
          fontSize="12"
          fontFamily="Arial, sans-serif"
        >
          {{ url }}
        </text>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M265.5 33.8984C265.641 33.8984 265.852 33.8516 266.047 33.7422C270.547 31.2969 272.109 30.1641 272.109 27.3203V21.4219C272.109 20.4844 271.742 20.1484 270.961 19.8125C270.094 19.4453 267.18 18.4297 266.328 18.1406C266.07 18.0547 265.766 18 265.5 18C265.234 18 264.93 18.0703 264.672 18.1406C263.82 18.3828 260.906 19.4531 260.039 19.8125C259.258 20.1406 258.891 20.4844 258.891 21.4219V27.3203C258.891 30.1641 260.461 31.2812 264.945 33.7422C265.148 33.8516 265.359 33.8984 265.5 33.8984ZM265.922 19.5781C266.945 19.9766 269.172 20.7656 270.344 21.1875C270.562 21.2656 270.617 21.3828 270.617 21.6641V27.0234C270.617 29.3125 269.469 29.9375 265.945 32.0625C265.727 32.1875 265.617 32.2344 265.508 32.2344V19.4844C265.617 19.4844 265.734 19.5156 265.922 19.5781Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M936.273 24.9766C936.5 24.9766 936.68 24.9062 936.82 24.7578L940.023 21.5312C940.195 21.3594 940.273 21.1719 940.273 20.9531C940.273 20.7422 940.188 20.5391 940.023 20.3828L936.82 17.125C936.68 16.9688 936.5 16.8906 936.273 16.8906C935.852 16.8906 935.516 17.2422 935.516 17.6719C935.516 17.8828 935.594 18.0547 935.727 18.2031L937.594 20.0312C937.227 19.9766 936.852 19.9453 936.477 19.9453C932.609 19.9453 929.516 23.0391 929.516 26.9141C929.516 30.7891 932.633 33.9062 936.5 33.9062C940.375 33.9062 943.484 30.7891 943.484 26.9141C943.484 26.4453 943.156 26.1094 942.688 26.1094C942.234 26.1094 941.93 26.4453 941.93 26.9141C941.93 29.9297 939.516 32.3516 936.5 32.3516C933.492 32.3516 931.07 29.9297 931.07 26.9141C931.07 23.875 933.469 21.4688 936.477 21.4688C936.984 21.4688 937.453 21.5078 937.867 21.5781L935.734 23.6875C935.594 23.8281 935.516 24 935.516 24.2109C935.516 24.6406 935.852 24.9766 936.273 24.9766Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M1134 33.0156C1134.49 33.0156 1134.89 32.6094 1134.89 32.1484V27.2578H1139.66C1140.13 27.2578 1140.54 26.8594 1140.54 26.3672C1140.54 25.8828 1140.13 25.4766 1139.66 25.4766H1134.89V20.5859C1134.89 20.1172 1134.49 19.7188 1134 19.7188C1133.52 19.7188 1133.11 20.1172 1133.11 20.5859V25.4766H1128.34C1127.88 25.4766 1127.46 25.8828 1127.46 26.3672C1127.46 26.8594 1127.88 27.2578 1128.34 27.2578H1133.11V32.1484C1133.11 32.6094 1133.52 33.0156 1134 33.0156Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M1161.8 31.0703H1163.23V32.375C1163.23 34.0547 1164.12 34.9219 1165.81 34.9219H1174.2C1175.89 34.9219 1176.77 34.0547 1176.77 32.3828V24.0469C1176.77 22.375 1175.89 21.5 1174.2 21.5H1172.77V20.2578C1172.77 18.5859 1171.88 17.7109 1170.19 17.7109H1161.8C1160.1 17.7109 1159.23 18.5781 1159.23 20.2578V28.5234C1159.23 30.1953 1160.1 31.0703 1161.8 31.0703ZM1161.9 29.5078C1161.18 29.5078 1160.78 29.1328 1160.78 28.3828V20.3984C1160.78 19.6406 1161.18 19.2656 1161.9 19.2656H1170.09C1170.8 19.2656 1171.2 19.6406 1171.2 20.3984V21.5H1165.81C1164.12 21.5 1163.23 22.375 1163.23 24.0469V29.5078H1161.9ZM1165.91 33.3672C1165.19 33.3672 1164.8 32.9922 1164.8 32.2422V24.1875C1164.8 23.4297 1165.19 23.0625 1165.91 23.0625H1174.1C1174.81 23.0625 1175.21 23.4297 1175.21 24.1875V32.2422C1175.21 32.9922 1174.81 33.3672 1174.1 33.3672H1165.91Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M1099.51 28.4141C1099.91 28.4141 1100.24 28.0859 1100.24 27.6953V19.8359L1100.18 18.6797L1100.66 19.25L1101.75 20.4141C1101.88 20.5547 1102.06 20.625 1102.24 20.625C1102.6 20.625 1102.9 20.3672 1102.9 20C1102.9 19.8047 1102.82 19.6641 1102.69 19.5312L1100.06 17.0078C1099.88 16.8203 1099.7 16.7578 1099.51 16.7578C1099.32 16.7578 1099.14 16.8203 1098.95 17.0078L1096.33 19.5312C1096.2 19.6641 1096.12 19.8047 1096.12 20C1096.12 20.3672 1096.41 20.625 1096.77 20.625C1096.95 20.625 1097.14 20.5547 1097.27 20.4141L1098.35 19.25L1098.84 18.6719L1098.78 19.8359V27.6953C1098.78 28.0859 1099.11 28.4141 1099.51 28.4141ZM1095 34.6562H1104C1105.7 34.6562 1106.57 33.7812 1106.57 32.1094V24.4297C1106.57 22.7578 1105.7 21.8828 1104 21.8828H1101.89V23.4375H1103.9C1104.61 23.4375 1105.02 23.8125 1105.02 24.5625V31.9688C1105.02 32.7188 1104.61 33.0938 1103.9 33.0938H1095.1C1094.38 33.0938 1093.98 32.7188 1093.98 31.9688V24.5625C1093.98 23.8125 1094.38 23.4375 1095.1 23.4375H1097.13V21.8828H1095C1093.31 21.8828 1092.43 22.75 1092.43 24.4297V32.1094C1092.43 33.7812 1093.31 34.6562 1095 34.6562Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M99.5703 33.6016H112.938C114.633 33.6016 115.516 32.7266 115.516 31.0547V21.5469C115.516 19.875 114.633 19 112.938 19H99.5703C97.8828 19 97 19.8672 97 21.5469V31.0547C97 32.7266 97.8828 33.6016 99.5703 33.6016ZM99.6719 32.0469C98.9531 32.0469 98.5547 31.6719 98.5547 30.9141V21.6875C98.5547 20.9297 98.9531 20.5547 99.6719 20.5547H103.234V32.0469H99.6719ZM112.836 20.5547C113.555 20.5547 113.953 20.9297 113.953 21.6875V30.9141C113.953 31.6719 113.555 32.0469 112.836 32.0469H104.711V20.5547H112.836ZM101.703 23.4141C101.984 23.4141 102.219 23.1719 102.219 22.9062C102.219 22.6406 101.984 22.4062 101.703 22.4062H100.102C99.8203 22.4062 99.5859 22.6406 99.5859 22.9062C99.5859 23.1719 99.8203 23.4141 100.102 23.4141H101.703ZM101.703 25.5156C101.984 25.5156 102.219 25.2812 102.219 25.0078C102.219 24.7422 101.984 24.5078 101.703 24.5078H100.102C99.8203 24.5078 99.5859 24.7422 99.5859 25.0078C99.5859 25.2812 99.8203 25.5156 100.102 25.5156H101.703ZM101.703 27.6094C101.984 27.6094 102.219 27.3828 102.219 27.1094C102.219 26.8438 101.984 26.6172 101.703 26.6172H100.102C99.8203 26.6172 99.5859 26.8438 99.5859 27.1094C99.5859 27.3828 99.8203 27.6094 100.102 27.6094H101.703Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M143.914 32.5938C144.094 32.7656 144.312 32.8594 144.562 32.8594C145.086 32.8594 145.492 32.4531 145.492 31.9375C145.492 31.6797 145.391 31.4453 145.211 31.2656L139.742 25.9219L145.211 20.5938C145.391 20.4141 145.492 20.1719 145.492 19.9219C145.492 19.4062 145.086 19 144.562 19C144.312 19 144.094 19.0938 143.922 19.2656L137.844 25.2031C137.625 25.4062 137.516 25.6562 137.516 25.9297C137.516 26.2031 137.625 26.4375 137.836 26.6484L143.914 32.5938Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <g class="mix-blend-luminosity">
        <path
          d="M168.422 32.8594C168.68 32.8594 168.891 32.7656 169.07 32.5938L175.148 26.6562C175.359 26.4375 175.469 26.2109 175.469 25.9297C175.469 25.6562 175.367 25.4141 175.148 25.2109L169.07 19.2656C168.891 19.0938 168.68 19 168.422 19C167.898 19 167.492 19.4062 167.492 19.9219C167.492 20.1719 167.602 20.4141 167.773 20.5938L173.25 25.9375L167.773 31.2656C167.594 31.4531 167.492 31.6797 167.492 31.9375C167.492 32.4531 167.898 32.8594 168.422 32.8594Z"
          fill="#A3A3A3"
        ></path>
      </g>
      <image
        width="1200"
        height="700"
        x="1"
        y="52"
        preserveAspectRatio="xMidYMid slice"
        :href="src"
        style="clip-path: url(#roundedBottom)"
      ></image>
    </g>
    <defs>
      <clipPath id="path0">
        <rect
          fill="white"
          :width="width"
          :height="height"
        ></rect>
      </clipPath>
      <clipPath id="roundedBottom">
        <path
          d="M1 52H1201V741C1201 747.075 1196.08 752 1190 752H12C5.92486 752 1 747.075 1 741V52Z"
          fill="white"
        ></path>
      </clipPath>
    </defs>
  </svg>
</template>

<script lang="ts" setup>
type SafariMockupProps = {
  url?: string;
  src?: string;
  width?: number;
  height?: number;
};

withDefaults(defineProps<SafariMockupProps>(), {
  width: 1203,
  height: 753,
});
</script>

```

## API

| Prop Name | Type     | Default | Description                                    |
| --------- | -------- | ------- | ---------------------------------------------- |
| `url`     | `string` | `null`  | URL displayed in the mockup's address bar.     |
| `src`     | `string` | `null`  | URL of the image to display inside the mockup. |
| `width`   | `number` | `1203`  | Width of the mockup SVG in pixels.             |
| `height`  | `number` | `753`   | Height of the mockup SVG in pixels.            |

## Features

- **Realistic Safari Browser Mockup**: Provides an SVG representation of the Safari browser window, ideal for showcasing website designs.
- **Customizable Dimensions**: Adjust the `width` and `height` props to fit your specific needs.
- **Address Bar URL Display**: Use the `url` prop to display a custom URL in the mockup's address bar.
- **Image Display Support**: Use the `src` prop to insert any image into the mockup's content area.
- **Light and Dark Mode Compatibility**: The mockup adapts its colors based on the theme.

## Credits

- Ported from [Magic UI](https://magicui.design/docs/components/safari).

URL: https://inspira-ui.com/components/index

---
title: Component Index
description: List of all the components provided by Inspira UI.
navigation: false
---

## All components

::ComponentIndex
::

URL: https://inspira-ui.com/components/input-and-forms/file-upload

---
title: File Upload
description: A modern file upload component with a 3D card effect, drag-and-drop functionality, and a responsive grid background pattern.
---

```vue
<template>
  <div class="space-y-6 p-8 dark:bg-black">
    <FileUpload class="rounded-lg border border-dashed border-neutral-200 dark:border-neutral-800">
      <FileUploadGrid />
    </FileUpload>
  </div>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="file-upload" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="FileUpload.vue" language="vue" componentName="FileUpload" type="ui" id="file-upload"}
:CodeViewerTab{label="FileUploadGrid.vue" language="vue" componentName="FileUploadGrid" type="ui" id="file-upload"}

::

## API

### `FileUpload`

The `FileUpload` component serves as a wrapper for the file upload effect. It manages mouse events to create a 3D perspective.

#### Props

| Prop Name | Type   | Default | Description                                           |
| --------- | ------ | ------- | ----------------------------------------------------- |
| `class`   | String | -       | Additional classes for styling the container element. |

#### Emits

| Event Name | Type                      | Description                                                |
| ---------- | ------------------------- | ---------------------------------------------------------- |
| `onChange` | `(files: File[]) => void` | Callback function triggered when files are added/uploaded. |

#### Usage

```vue [MyComponent.vue]
<FileUpload class="additional-class">
  <!-- Your content here -->
</FileUpload>
```

### `FileUploadGrid`

The `FileUploadGrid` component provides the background grid pattern for the file upload area. It is intended to be used within a `FileUpload` component to create the visual grid effect behind the upload interface.

#### Props

| Prop Name | Type   | Default | Description                            |
| --------- | ------ | ------- | -------------------------------------- |
| `class`   | String | -       | Additional classes for custom styling. |

#### Usage

```vue [MyComponent.vue]
<FileUploadGrid class="custom-class" />
```

## Credits

- Credits to [kalix127](https://github.com/kalix127) for porting this component.
- Inspired by [AcernityUI](https://ui.aceternity.com/components/file-upload).

URL: https://inspira-ui.com/components/input-and-forms/input

---
title: Input
description: A versatile and visually dynamic input field with radial hover effects, styled for modern web applications.
---

```vue
<template>
  <div class="flex h-56 w-full flex-col items-center justify-center gap-2">
    <label
      for="inputDemo"
      class="ml-4 w-full max-w-sm text-sm font-medium text-gray-700 dark:text-gray-200"
      >Hover over below input</label
    >
    <IInput
      id="inputDemo"
      placeholder="Hover over me"
      container-class="w-full max-w-sm"
    ></IInput>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="input" />
```

## Install Manually

Copy and paste the following code

```vue
<!-- Uses base code from shadcn-vue Input component and extends it's functionality-->
<template>
  <div
    ref="inputContainerRef"
    :class="cn('group/input rounded-lg p-[2px] transition duration-300', props.containerClass)"
    :style="{
      background: containerBg,
    }"
    @mouseenter="() => (visible = true)"
    @mouseleave="() => (visible = false)"
    @mousemove="handleMouseMove"
  >
    <input
      v-bind="$attrs"
      v-model="modelValue"
      :class="
        cn(
          `flex h-10 w-full border-none bg-gray-50 dark:bg-zinc-800 text-black dark:text-white shadow-input rounded-md px-3 py-2 text-sm  file:border-0 file:bg-transparent 
          file:text-sm file:font-medium placeholder:text-neutral-400 dark:placeholder-text-neutral-600 
          focus-visible:outline-none focus-visible:ring-[2px]  focus-visible:ring-neutral-400 dark:focus-visible:ring-neutral-600
           disabled:cursor-not-allowed disabled:opacity-50
           dark:shadow-[0px_0px_1px_1px_var(--neutral-700)]
           group-hover/input:shadow-none transition duration-400`,
          props.class,
        )
      "
    />
  </div>
</template>

<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { useVModel } from "@vueuse/core";
import { ref, computed } from "vue";

defineOptions({
  inheritAttrs: false,
});

const props = defineProps<{
  defaultValue?: string | number;
  modelValue?: string | number;
  class?: HTMLAttributes["class"];
  containerClass?: HTMLAttributes["class"];
}>();

const emits = defineEmits<{
  (e: "update:modelValue", payload: string | number): void;
}>();

const modelValue = useVModel(props, "modelValue", emits, {
  passive: true,
  defaultValue: props.defaultValue,
});

const inputContainerRef = ref<HTMLDivElement | null>(null);
const mouse = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const radius = 100;
const visible = ref(false);

const containerBg = computed(() => {
  return `
        radial-gradient(
          ${visible.value ? radius + "px" : "0px"} circle at ${mouse.value.x}px ${mouse.value.y}px,
          var(--blue-500),
          transparent 80%
        )
      `;
});

function handleMouseMove({ clientX, clientY }: MouseEvent) {
  if (!inputContainerRef.value) return;

  const { left, top } = inputContainerRef.value.getBoundingClientRect();
  mouse.value = { x: clientX - left, y: clientY - top };
}
</script>

<style scoped>
input {
  box-shadow:
    0px 2px 3px -1px rgba(0, 0, 0, 0.1),
    0px 1px 0px 0px rgba(25, 28, 33, 0.02),
    0px 0px 0px 1px rgba(25, 28, 33, 0.08);
}
</style>

```

## API

| Prop Name        | Type                | Default | Description                                                 |
| ---------------- | ------------------- | ------- | ----------------------------------------------------------- |
| `defaultValue`   | `string  \| number` | `""`    | Default value of the input field.                           |
| `class`          | `string`            | `""`    | Additional CSS classes for custom styling.                  |
| `containerClass` | `string`            | `""`    | Additional CSS classes for custom styling of the container. |

## Features

- **Radial Hover Effect**: Displays a dynamic radial gradient background that follows mouse movements when hovering over the input container.
- **Two-Way Binding**: Supports `v-model` for seamless integration with Vue's two-way data binding.
- **Dark Mode Compatibility**: Automatically adapts to dark mode styles using Tailwind's `dark:` utilities.
- **Customizable Styles**: Easily extend or override styles using the `class` prop.
- **Accessible Focus Ring**: Includes focus-visible styles for enhanced accessibility and usability.
- **Responsive Design**: Works well across different screen sizes and devices.

## Styles

This component inherits base styles from ShadCN Vue’s Input component and applies additional functionality, including hover effects and shadow styling.

## Credits

- Built on ShadCN Vue's Input component foundation, with extended functionality for modern UI/UX needs.
- Ported from [Aceternity UI's Signup Form Input Component](https://ui.aceternity.com/components/signup-form)

URL: https://inspira-ui.com/components/input-and-forms/placeholders-and-vanish-input

---
title: Placeholders And Vanish Input
description: Sliding in placeholders and vanish effect of input on submit
---

```vue
<template>
  <div class="flex h-[40rem] flex-col items-center justify-center px-4">
    <h2 class="mb-10 text-center text-xl text-black sm:mb-20 sm:text-5xl dark:text-white">
      Ask Anything Silly
    </h2>
    <VanishingInput
      v-model="text"
      :placeholders="placeholders"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const placeholders = [
  "Why is my code always broken?",
  "What does 'undefined' even mean?",
  "How to center a div (for real this time)",
  "Am I smarter than a compiler?",
  "Do loops ever get dizzy?",
];
const text = ref("");
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="vanishing-input" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <form
    :class="[
      'relative mx-auto h-12 w-full max-w-xl overflow-hidden rounded-full bg-white shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),_0px_1px_0px_0px_rgba(25,28,33,0.02),_0px_0px_0px_1px_rgba(25,28,33,0.08)] transition duration-200 dark:bg-zinc-800',
      vanishingText && 'bg-gray-50',
    ]"
    @submit.prevent="handleSubmit"
  >
    <!-- Canvas Element -->
    <canvas
      ref="canvasRef"
      :class="[
        'pointer-events-none absolute left-2 top-[20%] origin-top-left scale-50 pr-20 text-base invert sm:left-8 dark:invert-0',
        animating ? 'opacity-100' : 'opacity-0',
      ]"
    />

    <!-- Text Input -->
    <input
      ref="inputRef"
      v-model="vanishingText"
      :disabled="animating"
      type="text"
      class="relative z-50 size-full rounded-full border-none bg-transparent pl-4 pr-20 text-sm text-black focus:outline-none focus:ring-0 sm:pl-10 sm:text-base dark:text-white"
      :class="{ 'text-transparent dark:text-transparent': animating }"
      @keydown.enter="handleKeyDown"
    />

    <!-- Submit Button -->
    <button
      :disabled="!vanishingText"
      type="submit"
      class="absolute right-2 top-1/2 z-50 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-black transition duration-200 disabled:bg-gray-100 dark:bg-zinc-900 dark:disabled:bg-zinc-700"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        class="size-4 text-gray-300"
      >
        <path
          stroke="none"
          d="M0 0h24v24H0z"
          fill="none"
        />
        <path
          d="M5 12l14 0"
          :style="{
            strokeDasharray: '50%',
            strokeDashoffset: vanishingText ? '0' : '50%',
            transition: 'stroke-dashoffset 0.3s linear',
          }"
        />
        <path d="M13 18l6 -6" />
        <path d="M13 6l6 6" />
      </svg>
    </button>

    <!-- Placeholder Text -->
    <div class="pointer-events-none absolute inset-0 flex items-center rounded-full">
      <Transition
        v-show="!vanishingText"
        mode="out-in"
        enter-active-class="transition duration-300 ease-out"
        leave-active-class="transition duration-300 ease-in"
        enter-from-class="opacity-0 translate-y-4"
        enter-to-class="opacity-100 translate-y-0"
        leave-from-class="opacity-100 translate-y-0"
        leave-to-class="opacity-0 -translate-y-4"
      >
        <p
          :key="currentPlaceholder"
          class="w-[calc(100%-2rem)] truncate pl-4 text-left text-sm font-normal text-neutral-500 sm:pl-10 sm:text-base dark:text-zinc-500"
        >
          {{ placeholders[currentPlaceholder] }}
        </p>
      </Transition>
    </div>
  </form>
</template>

<script setup lang="ts">
import { ref, onMounted, watch, onBeforeUnmount } from "vue";
import { templateRef } from "@vueuse/core";

// Define interfaces for props and data structures
interface Props {
  placeholders: string[];
}

interface PixelData {
  x: number;
  y: number;
  color: string;
}

interface AnimatedPixel extends PixelData {
  r: number;
}

const vanishingText = defineModel<string>({
  default: "",
});
const emit = defineEmits(["submit", "change"]);

const canvasRef = templateRef<HTMLCanvasElement>("canvasRef");
const inputRef = templateRef<HTMLInputElement>("inputRef");

// normal refs
const currentPlaceholder = ref<number>(0);
const animating = ref<boolean>(false);
const intervalRef = ref<number | null>(null);
const newDataRef = ref<AnimatedPixel[]>([]);
const animationFrame = ref<number | null>(null);

// props
const props = withDefaults(defineProps<Props>(), {
  placeholders: () => ["Placeholder 1", "Placeholder 2", "Placeholder 3"],
});

// Focus on input when mounted
onMounted(() => {
  if (!inputRef.value) return;
  inputRef.value.focus();
});

function changePlaceholder(): void {
  intervalRef.value = window.setInterval(() => {
    currentPlaceholder.value = (currentPlaceholder.value + 1) % props.placeholders.length;
  }, 3000);
}

function handleVisibilityChange(): void {
  if (document.visibilityState !== "visible" && intervalRef.value) {
    clearInterval(intervalRef.value);
    intervalRef.value = null;
  } else if (document.visibilityState === "visible") {
    changePlaceholder();
  }
}

function draw(): void {
  if (!inputRef.value || !canvasRef.value) return;

  const canvas = canvasRef.value;
  const ctx = canvas.getContext("2d");
  if (!ctx) return;

  const computedStyles = getComputedStyle(inputRef.value);

  canvas.width = 800;
  canvas.height = 800;
  ctx.clearRect(0, 0, 800, 800);

  const fontSize = parseFloat(computedStyles.getPropertyValue("font-size"));
  ctx.font = `${fontSize * 2}px ${computedStyles.fontFamily}`;
  ctx.fillStyle = "#FFF";
  ctx.fillText(vanishingText.value, 16, 40);

  const imageData = ctx.getImageData(0, 0, 800, 800);
  const pixelData = imageData.data;
  const newData: PixelData[] = [];

  for (let t = 0; t < 800; t++) {
    let i = 4 * t * 800;
    for (let n = 0; n < 800; n++) {
      let e = i + 4 * n;
      if (pixelData[e] !== 0 && pixelData[e + 1] !== 0 && pixelData[e + 2] !== 0) {
        newData.push({
          x: n,
          y: t,
          color: `rgba(${pixelData[e]}, ${pixelData[e + 1]}, ${pixelData[e + 2]}, ${pixelData[e + 3]})`,
        });
      }
    }
  }
  newDataRef.value = newData.map(({ x, y, color }) => ({ x, y, r: 1, color }));
}

function animate(start: number = 0): void {
  animationFrame.value = requestAnimationFrame(() => {
    const newArr: AnimatedPixel[] = [];
    for (const current of newDataRef.value) {
      if (current.x < start) {
        newArr.push(current);
      } else {
        if (current.r <= 0) {
          current.r = 0;
          continue;
        }
        current.x += Math.random() > 0.5 ? 1 : -1;
        current.y += Math.random() > 0.5 ? 1 : -1;
        current.r -= 0.05 * Math.random();
        newArr.push(current);
      }
    }
    newDataRef.value = newArr;
    const ctx = canvasRef.value?.getContext("2d");
    if (ctx) {
      ctx.clearRect(start, 0, 800, 800);
      newDataRef.value.forEach(({ x, y, r, color }) => {
        if (x > start) {
          ctx.beginPath();
          ctx.rect(x, y, r, r);
          ctx.fillStyle = color;
          ctx.strokeStyle = color;
          ctx.stroke();
        }
      });
    }
    if (newDataRef.value.length > 0) {
      animate(start - 8);
    } else {
      vanishingText.value = "";
      animating.value = false;
      setTimeout(() => {
        // regain focus after animation
        inputRef.value.focus();
      }, 100);
    }
  });
}

function handleKeyDown(e: KeyboardEvent): void {
  if (e.key === "Enter" && !animating.value) {
    vanishAndSubmit();
  }
}

function vanishAndSubmit(): void {
  animating.value = true;
  draw();
  if (vanishingText.value) {
    const maxX = Math.max(...newDataRef.value.map(({ x }) => x));
    animate(maxX);
    emit("submit", vanishingText.value);
  }
}

function handleSubmit(): void {
  vanishAndSubmit();
}

// Watch for value changes
watch(vanishingText, (newVal: string) => {
  if (!animating.value) {
    emit("change", { target: { value: newVal } });
  }
});

onMounted(() => {
  changePlaceholder();
  document.addEventListener("visibilitychange", handleVisibilityChange);
});

onBeforeUnmount(() => {
  if (intervalRef.value) {
    clearInterval(intervalRef.value);
  }
  if (animationFrame.value) {
    cancelAnimationFrame(animationFrame.value);
  }
  document.removeEventListener("visibilitychange", handleVisibilityChange);
});
</script>

```

## API

| Prop Name      | Type            | Default                                               | Description                                                                     |
| -------------- | --------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
| `placeholders` | `Array<string>` | `["Placeholder 1", "Placeholder 2", "Placeholder 3"]` | An array of placeholder texts that cycle through as prompts in the input field. |

This component listens to the following events emitted by the `VanishingInput` component:

| Event Name | Parameters | Description                             |
| ---------- | ---------- | --------------------------------------- |
| `change`   | `Event`    | Triggered when the input value changes. |
| `submit`   | `string`   | Triggered when the input is submitted.  |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for porting this component.

- Ported from [Aceternity UI's Placeholders And Vanish Input](https://ui.aceternity.com/components/placeholders-and-vanish-input).

URL: https://inspira-ui.com/components/miscellaneous/animate-grid

---
title: Animate Grid
description: Skew Animation grid with box shadow.
---

```vue
<template>
  <div class="flex items-center justify-center p-4">
    <AnimateGrid :cards>
      <template #logo="{ logo }">
        <img
          class="logo mx-auto h-10 w-auto"
          :src="logo"
        />
      </template>
    </AnimateGrid>
  </div>
</template>
<script lang="ts" setup>
import cloudflare from "./cloudflare.svg";

const cards = Array.from({ length: 16 }, () => ({
  logo: cloudflare,
}));
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="animate-grid" />
```

## Install Manually

::steps{level=4}

Copy and paste the following code

::CodeViewer{filename="AnimateGrid.vue" language="vue" componentName="AnimateGrid" type="ui" id="animate-grid"}
::

#### Add SVG file

Add at least one SVG file to the same folder as your component and update the import in your component to use it

::

## API

| Prop Name            | Type     | Default             | Description                                         |
| -------------------- | -------- | ------------------- | --------------------------------------------------- |
| `textGlowStartColor` | `string` | `"#38ef7d80"`       | Color of the box shadow start.                      |
| `textGlowEndColor`   | `string` | `"#38ef7d"`         | Color of the box shadow end.                        |
| `perspective`        | `number` | `600`               | You can pass perspective to transform CSS property. |
| `rotateX`            | `number` | `-1`                | You can pass rotateX to transform CSS property.     |
| `rotateY`            | `number` | `-15`               | You can pass rotateY to transform CSS property.     |
| `cards`              | `[]`     | `"[{logo: 'src'}]"` | Cards to display in grid.                           |
| `class`              | `string` | `""`                | Additional tailwind CSS classes for custom styling. |

## Features

- **Slot-Based Content**: Supports a default slot to add content inside the Animate Grid container.

## Credits

- Thanks to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for providing this component.

URL: https://inspira-ui.com/components/miscellaneous/animated-circular-progressbar

---
title: Animated Circular Progress Bar
description: Animated Circular Progress Bar is a component that displays a circular gauge with a percentage value.
---

```vue
<template>
  <div class="flex items-center justify-center py-1">
    <AnimatedCircularProgressBar
      :max="100"
      :min="0"
      :value="value"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";

const value = ref(0);

function handleIncrement(prev: number) {
  return prev === 100 ? 0 : prev + 10;
}

onMounted(() => {
  value.value = handleIncrement(value.value);
  const interval = setInterval(() => {
    value.value = handleIncrement(value.value);
  }, 2000);

  onBeforeUnmount(() => clearInterval(interval));
});
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="animated-circular-progressbar" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    class="progress-circle-base"
    :class="cn('relative size-40 text-2xl font-semibold', props.class)"
  >
    <svg
      fill="none"
      class="size-full"
      stroke-width="2"
      viewBox="0 0 100 100"
    >
      <circle
        v-if="currentPercent <= 90 && currentPercent >= 0"
        cx="50"
        cy="50"
        r="45"
        :stroke-width="circleStrokeWidth"
        stroke-dashoffset="0"
        stroke-linecap="round"
        stroke-linejoin="round"
        class="gauge-secondary-stroke opacity-100"
      />
      <circle
        cx="50"
        cy="50"
        r="45"
        :stroke-width="circleStrokeWidth"
        stroke-dashoffset="0"
        stroke-linecap="round"
        stroke-linejoin="round"
        class="gauge-primary-stroke opacity-100"
      />
    </svg>
    <span
      v-if="showPercentage"
      :data-current-value="currentPercent"
      class="absolute inset-0 m-auto size-fit delay-0 duration-1000 ease-linear animate-in fade-in"
    >
      {{ currentPercent }}
    </span>
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { cn } from "@/lib/utils";

interface Props {
  max?: number;
  value?: number;
  min?: number;
  gaugePrimaryColor?: string;
  gaugeSecondaryColor?: string;
  class?: string;
  circleStrokeWidth?: number;
  showPercentage?: boolean;
  duration?: number;
}

const props = withDefaults(defineProps<Props>(), {
  max: 100,
  min: 0,
  value: 0,
  gaugePrimaryColor: "rgb(79 70 229)",
  gaugeSecondaryColor: "rgba(0, 0, 0, 0.1)",
  circleStrokeWidth: 10,
  showPercentage: true,
  duration: 1,
});

const circumference = 2 * Math.PI * 45;
const percentPx = circumference / 100;

const currentPercent = computed(() => ((props.value - props.min) / (props.max - props.min)) * 100);
const percentageInPx = computed(() => `${percentPx}px`);
const durationInSeconds = computed(() => `${props.duration}s`);
</script>

<style scoped lang="css">
.progress-circle-base {
  --circle-size: 100px;
  --circumference: v-bind(circumference);
  --percent-to-px: v-bind(percentageInPx);
  --gap-percent: 5;
  --offset-factor: 0;
  --transition-step: 200ms;
  --percent-to-deg: 3.6deg;
  transform: translateZ(0);
}

.gauge-primary-stroke {
  stroke: v-bind(gaugePrimaryColor);
  --stroke-percent: v-bind(currentPercent);
  stroke-dasharray: calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference);
  transition:
    v-bind(durationInSeconds) ease,
    stroke v-bind(durationInSeconds) ease;
  transition-property: stroke-dasharray, transform;
  transform: rotate(
    calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg))
  );
  transform-origin: calc(var(--circle-size) / 2) calc(var(--circle-size) / 2);
}

.gauge-secondary-stroke {
  stroke: v-bind(gaugeSecondaryColor);
  --stroke-percent: 90 - v-bind(currentPercent);
  --offset-factor-secondary: calc(1 - var(--offset-factor));
  stroke-dasharray: calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference);
  transform: rotate(
      calc(
        1turn - 90deg -
          (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary))
      )
    )
    scaleY(-1);
  transition: all v-bind(durationInSeconds) ease;
  transform-origin: calc(var(--circle-size) / 2) calc(var(--circle-size) / 2);
}
</style>

```

## API

| Prop Name             | Type      | Default              | Description                                 |
| --------------------- | --------- | -------------------- | ------------------------------------------- |
| `class`               | `string`  | `-`                  | The class to be applied to the component.   |
| `max`                 | `number`  | `100`                | The maximum value of the gauge.             |
| `min`                 | `number`  | `0`                  | The minimum value of the gauge.             |
| `value`               | `number`  | `0`                  | The current value of the gauge.             |
| `gaugePrimaryColor`   | `string`  | `rgb(79 70 229)`     | The primary color of the gauge.             |
| `gaugeSecondaryColor` | `string`  | `rgba(0, 0, 0, 0.1)` | The secondary color of the gauge.           |
| `circleStrokeWidth`   | `number`  | `10`                 | The width of the circle progress bar.       |
| `showPercentage`      | `boolean` | `true`               | Show the value inside the circle            |
| `duration`            | `number`  | `1`                  | The duration of the animation (in seconds). |

## Credits

- Credits to [Magic UI](https://magicui.design/docs/components/animated-circular-progress-bar).
- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for porting this component.

URL: https://inspira-ui.com/components/miscellaneous/animated-list

---
title: Animated List
description: A sequentially animated list that introduces each item with a timed delay, perfect for displaying notifications or events on your landing page.
---

```vue
<template>
  <ClientOnly>
    <div class="relative flex h-[500px] w-full flex-col overflow-hidden p-6">
      <AnimatedList>
        <template #default>
          <Notification
            v-for="(item, idx) in notifications"
            :key="idx"
            :name="item.name"
            :description="item.description"
            :icon="item.icon"
            :color="item.color"
            :time="item.time"
          />
        </template>
      </AnimatedList>
    </div>
  </ClientOnly>
</template>

<script setup lang="ts">
const notifications = [
  {
    name: "Payment received",
    description: "Inspira UI",
    time: "15m ago",
    icon: "💸",
    color: "#00C9A7",
  },
  {
    name: "User signed up",
    description: "Inspira UI",
    time: "10m ago",
    icon: "👤",
    color: "#FFB800",
  },
  {
    name: "New message",
    description: "Inspira UI",
    time: "5m ago",
    icon: "💬",
    color: "#FF3D71",
  },
  {
    name: "New event",
    description: "Inspira UI",
    time: "2m ago",
    icon: "🗞️",
    color: "#1E86FF",
  },
  {
    name: "Task completed",
    description: "Inspira UI",
    time: "1m ago",
    icon: "✅",
    color: "#45B26B",
  },
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="animated-list" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="AnimatedList.vue" language="vue" componentName="AnimatedList" type="ui" id="animated-list"}
:CodeViewerTab{label="Notification.vue" language="vue" componentName="Notification" type="ui" id="animated-list"}
::

## API

| Prop Name | Type     | Default | Description                                                    |
| --------- | -------- | ------- | -------------------------------------------------------------- |
| `delay`   | `number` | `1`     | The delay in milliseconds before adding each item to the list. |

## Features

- **Animated Item Loading**: Items are added to the list one by one with a configurable delay.
- **Smooth Transitions**: Each item animates in with a spring effect on entry and a smooth scale and opacity animation on exit.
- **Reverse Order**: Items are displayed in reverse order (newest at the top) for a dynamic, engaging user experience.
- **Flexibility**: You can pass different components as items, making it highly versatile.

## Credits

- Inspired by [Magic UI](https://magicui.design/docs/components/animated-list).

URL: https://inspira-ui.com/components/miscellaneous/animated-testimonials

---
title: Animated Testimonials
description: An engaging and animated testimonial component showcasing user feedback with transitions and auto-play functionality.
navBadges:
  - value: New
    type: lime
---

```vue
<template>
  <ClientOnly>
    <AnimatedTestimonials :testimonials="testimonials" />
  </ClientOnly>
</template>

<script lang="ts" setup>
const testimonials = [
  {
    quote:
      "The attention to detail and innovative features have completely transformed our workflow. This is exactly what we've been looking for.",
    name: "Sarah Chen",
    designation: "Product Manager at TechFlow",
    image:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=3560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    quote:
      "Implementation was seamless and the results exceeded our expectations. The platform's flexibility is remarkable.",
    name: "Michael Rodriguez",
    designation: "CTO at InnovateSphere",
    image:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    quote:
      "This solution has significantly improved our team's productivity. The intuitive interface makes complex tasks simple.",
    name: "Emily Watson",
    designation: "Operations Director at CloudScale",
    image:
      "https://images.unsplash.com/photo-1623582854588-d60de57fa33f?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    quote:
      "Outstanding support and robust features. It's rare to find a product that delivers on all its promises.",
    name: "James Kim",
    designation: "Engineering Lead at DataPro",
    image:
      "https://images.unsplash.com/photo-1636041293178-808a6762ab39?q=80&w=3464&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    quote:
      "The scalability and performance have been game-changing for our organization. Highly recommend to any growing business.",
    name: "Lisa Thompson",
    designation: "VP of Technology at FutureNet",
    image:
      "https://images.unsplash.com/photo-1624561172888-ac93c696e10c?q=80&w=2592&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
];
</script>

```

## API

| Prop Name      | Type            | Default | Description                                                                                    |
| -------------- | --------------- | ------- | ---------------------------------------------------------------------------------------------- |
| `testimonials` | `Testimonial[]` | `[]`    | An array of testimonial objects containing quote, name, image, and designation.                |
| `autoplay`     | `boolean`       | `false` | Whether to cycle through testimonials automatically.                                           |
| `duration`     | `number`        | `5000`  | Duration (in milliseconds) to wait before automatically transitioning to the next testimonial. |

### Testimonial Object

Each testimonial object must contain the following fields:

| Property      | Type     | Description                                                       |
| ------------- | -------- | ----------------------------------------------------------------- |
| `quote`       | `string` | The testimonial text.                                             |
| `name`        | `string` | The name of the person or entity providing the testimonial.       |
| `designation` | `string` | The position or role of the testimonial author (e.g., CEO, user). |
| `image`       | `string` | URL of the image or avatar for the testimonial author.            |

## Install using CLI

```vue
<InstallationCli component-id="animated-testimonials" />
```

## Install Manually

You can copy and paste the following code to create this component:

::code-group

::CodeViewerTab{label="AnimatedTestimonials.vue" language="vue" componentName="AnimatedTestimonials" type="ui" id="animated-testimonials"}
::

::

## Features

- **Animated Slides**: Utilizes Motion-V to animate transitions between testimonials.
- **Auto-Play Support**: Automatically transitions to the next testimonial after a specified duration.
- **Random Rotation Effects**: Adds a subtle randomized rotation for each new slide.
- **Navigation Buttons**: Manually cycle through testimonials using previous and next controls.
- **Responsive and Modular**: Adapts well to different screen sizes, allowing easy integration into various layouts.

## Credits

- Ported from (Aceternity UI Animated Testimonials)[https://ui.aceternity.com/components/animated-testimonials].

URL: https://inspira-ui.com/components/miscellaneous/animated-tooltip

---
title: Animated Tooltip
description: A cool tooltip that reveals on hover, follows mouse pointer
---

```vue
<template>
  <div class="mb-10 flex min-h-96 w-full flex-row items-center justify-center">
    <AnimatedTooltip :items="people" />
  </div>
</template>

<script setup lang="ts">
const people = [
  {
    id: 1,
    name: "John Doe",
    designation: "Software Engineer",
    image:
      "https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3387&q=80",
  },
  {
    id: 2,
    name: "Robert Johnson",
    designation: "Product Manager",
    image:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60",
  },
  {
    id: 3,
    name: "Jane Smith",
    designation: "Data Scientist",
    image:
      "https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60",
  },
  {
    id: 4,
    name: "Emily Davis",
    designation: "UX Designer",
    image:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60",
  },
  {
    id: 5,
    name: "Tyler Durden",
    designation: "Soap Developer",
    image:
      "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3540&q=80",
  },
  {
    id: 6,
    name: "Dora",
    designation: "The Explorer",
    image:
      "https://images.unsplash.com/photo-1544725176-7c40e5a71c5e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3534&q=80",
  },
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="animated-tooltip" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    v-for="item in items"
    :key="item.id"
    class="group relative -mr-4"
    @mouseenter="(e) => handleMouseEnter(e, item.id)"
    @mouseleave="hoveredIndex = null"
    @mousemove="handleMouseMove"
  >
    <!-- Tooltip -->
    <Motion
      v-if="hoveredIndex === item.id"
      :initial="{
        opacity: 0,
        y: 20,
        scale: 0.6,
      }"
      :animate="{
        opacity: 1,
        y: 0,
        scale: 1,
      }"
      :transition="{
        type: 'spring',
        stiffness: 260,
        damping: 10,
      }"
      :exit="{
        opacity: 0,
        y: 20,
        scale: 0.6,
      }"
      :style="{
        translate: `${translation}px`,
        rotate: `${rotation}deg`,
      }"
      class="absolute -left-1/2 -top-16 z-50 flex translate-x-1/2 flex-col items-center justify-center whitespace-nowrap rounded-md bg-black px-4 py-2 text-xs shadow-xl"
    >
      <div
        class="absolute inset-x-10 -bottom-px z-30 h-px w-1/5 bg-gradient-to-r from-transparent via-emerald-500 to-transparent"
      />
      <div
        class="absolute -bottom-px left-10 z-30 h-px w-2/5 bg-gradient-to-r from-transparent via-sky-500 to-transparent"
      />
      <div class="relative z-30 text-base font-bold text-white">
        {{ item.name }}
      </div>
      <div class="text-xs text-white">{{ item.designation }}</div>
    </Motion>

    <!-- Avatar Image -->
    <img
      :src="item.image"
      :alt="item.name"
      class="relative !m-0 size-14 rounded-full border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Motion } from "motion-v";

interface Item {
  id: number;
  name: string;
  designation: string;
  image: string;
}

defineProps<{
  items: Item[];
}>();

const hoveredIndex = ref<number | null>(null);
const mouseX = ref<number>(0);

// Calculate rotation and translation based on mouse position
const rotation = computed<number>(() => {
  const x = mouseX.value;
  return (x / 100) * 50;
});

const translation = computed<number>(() => {
  const x = mouseX.value;
  return (x / 100) * 50;
});

// Handle initial mouse position and hover
function handleMouseEnter(event: MouseEvent, itemId: number) {
  hoveredIndex.value = itemId;
  // Calculate initial position immediately
  const rect = (event.target as HTMLElement)?.getBoundingClientRect();
  const halfWidth = rect.width / 2;
  mouseX.value = event.clientX - rect.left - halfWidth;
}

// Handle mouse movement
function handleMouseMove(event: MouseEvent) {
  const rect = (event.target as HTMLElement)?.getBoundingClientRect();
  const halfWidth = rect.width / 2;
  mouseX.value = event.clientX - rect.left - halfWidth;
}
</script>

```

## API

| Prop Name | Type                                                                    | Default | Description                                                                                                                                 |
| --------- | ----------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `items`   | `Array<{id: number, name: string, designation: string, image: string}>` | `[]`    | An array of objects, each representing an item. Each object in the array should have the following properties: id, name, designation, image |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for this component.

- Inspired from [Aceternity UI's Animated Tooltip](https://ui.aceternity.com/components/animated-tooltip).

URL: https://inspira-ui.com/components/miscellaneous/balance-slider

---
title: Balance Slider
description: A dynamic balance slider with adjustable colors, limits, and interactive tooltip.
---

```vue
<template>
  <div class="w-full p-4">
    <BalanceSlider
      :right-color="rightColor"
      left-content="COFFEE"
      right-content="MILK"
      :indicator-color="indicatorColor"
    />
  </div>
</template>

<script lang="ts" setup>
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const isDark = computed(() => useColorMode().value == "dark");
const rightColor = computed(() => (isDark.value ? "#FFFFFF" : "#000000"));
const indicatorColor = computed(() => (isDark.value ? "#FFFFFF" : "#000000"));
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="balance-slider" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    class="slider-container relative mx-auto my-0 grid place-items-center overflow-hidden"
    :style="sliderStyles"
    @mouseenter="active = 1"
    @mouseleave="active = 0"
    @focusin="active = 1"
    @focusout="active = 0"
    @touchstart="active = 1"
    @touchend="active = 0"
  >
    <input
      id="track"
      v-model="value"
      type="range"
      min="0"
      max="100"
      class="size-full touch-none opacity-0 hover:cursor-grab focus-visible:outline-offset-4 focus-visible:outline-transparent active:cursor-grabbing"
    />
    <div
      aria-hidden="true"
      :class="
        cn('slider-value-labels pointer-events-none absolute inset-x-0 top-0 z-[2] h-1/2 text-base')
      "
      :style="sliderLabelStyles"
    ></div>
    <div
      class="slider-track pointer-events-none absolute bottom-0 w-full"
      :style="sliderTrackStyles"
    >
      <div
        class="slider-indicator absolute top-1/2 z-[2] h-3/4 w-1 -translate-x-1/2 -translate-y-1/2 rounded-sm"
      ></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { cn } from "@/lib/utils";

interface Props {
  initialValue?: number;
  leftColor?: string;
  rightColor?: string;
  minShiftLimit?: number;
  maxShiftLimit?: number;
  leftContent?: string;
  rightContent?: string;
  indicatorColor?: string;
  borderRadius?: number;
}

const props = withDefaults(defineProps<Props>(), {
  initialValue: 50,
  minShiftLimit: 40,
  maxShiftLimit: 68,
  leftContent: "LEFT",
  rightContent: "RIGHT",
  leftColor: "#e68a00",
  rightColor: "#ffffff",
  indicatorColor: "#FFFFFF",
  borderRadius: 8,
});

const value = ref(props.initialValue);
const active = ref(0);

const shift = computed(() =>
  value.value > props.minShiftLimit && value.value < props.maxShiftLimit ? 1 : 0,
);

const sliderStyles = computed(() => ({
  "--value": value.value,
  "--shift": shift.value,
  "--active": active.value,
  "--leftContent": `"${props.leftContent} "`,
  "--rightContent": `" ${props.rightContent}"`,
  "--indicatorColor": indicatorColorHsl.value,
}));

const sliderLabelStyles = computed(() => ({
  "--shift": shift.value,
}));

const sliderTrackStyles = computed(() => ({
  "--value": value.value,
  "--shift": shift.value,
  "--leftColor": leftColorHsl.value,
  "--rightColor": rightColorHsl.value,
}));

const leftColorHsl = computed(() => {
  const [h, s, l] = hexToHsl(props.leftColor);
  const alpha = 0.4;
  const lightness = 24 + (30 * (100 - value.value)) / 100;
  return `hsl(${h} ${s}% ${lightness}% / ${alpha})`;
});

const rightColorHsl = computed(() => {
  const [h, s, l] = hexToHsl(props.rightColor);
  const alpha = 0.1 + (0.4 * (100 - value.value)) / 100;
  return `hsl(${h} ${s}% ${l}% / ${alpha})`;
});

const indicatorColorHsl = computed(() => {
  const [h, s, l] = hexToHsl(props.indicatorColor); // Base color as hex, here white (#ffffff)
  const activeAlpha = active.value * 0.5 + 0.5; // Calculate alpha based on active state
  return `hsl(${h} ${s}% ${l}% / ${activeAlpha})`;
});

const borderRadiusInPx = computed(() => `${props.borderRadius}px`);

function hexToHsl(hex: string): [number, number, number] {
  // Remove "#" if present
  hex = hex.replace(/^#/, "");

  // Parse r, g, b values
  let r = parseInt(hex.substring(0, 2), 16) / 255;
  let g = parseInt(hex.substring(2, 4), 16) / 255;
  let b = parseInt(hex.substring(4, 6), 16) / 255;

  // Find min and max values of r, g, b
  let max = Math.max(r, g, b),
    min = Math.min(r, g, b);
  let h = 0,
    s = 0,
    l = (max + min) / 2;

  if (max != min) {
    let d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r:
        h = (g - b) / d + (g < b ? 6 : 0);
        break;
      case g:
        h = (b - r) / d + 2;
        break;
      case b:
        h = (r - g) / d + 4;
        break;
    }
    h /= 6;
  }

  return [h * 360, s * 100, l * 100];
}
</script>

<style scoped>
.slider-container {
  --speed: 0.65s;
  --update: 0s;
  --timing: linear(
    0,
    0.5007 7.21%,
    0.7803 12.29%,
    0.8883 14.93%,
    0.9724 17.63%,
    1.0343 20.44%,
    1.0754 23.44%,
    1.0898 25.22%,
    1.0984 27.11%,
    1.1014 29.15%,
    1.0989 31.4%,
    1.0854 35.23%,
    1.0196 48.86%,
    1.0043 54.06%,
    0.9956 59.6%,
    0.9925 68.11%,
    1
  );
}

.slider-value-labels {
  transform: translateY(calc(var(--shift, 0) * 50%));
  transition: transform var(--speed) var(--timing);
  counter-reset: low var(--value) high calc(100 - var(--value));
}

.slider-value-labels::after,
.slider-value-labels::before {
  font-variant: tabular-nums;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-weight: bold;
  color: white;
  font-family: monospace;
}

.slider-value-labels::before {
  --range: calc((70 - (var(--value) / 100 * 10)) * 1%);
  color: hsl(24 74% 54%);
  content: var(--leftContent) counter(low) "%";
  mask: linear-gradient(90deg, hsl(0 0% 100% / 0.6) var(--range), hsl(0 0% 100% / 1) var(--range));
  left: 0.5rem;
}

.slider-value-labels::after {
  --range: calc((50 - (var(--value) / 100 * 10)) * 1%);
  content: counter(high) "% " var(--rightContent);
  mask: linear-gradient(90deg, hsl(0 0% 100% / 1) var(--range), hsl(0 0% 100% / 0.5) var(--range));
  right: 0.5rem;
}

.slider-track {
  height: calc(50% + (var(--shift) * 50%));
  transition: height var(--speed) var(--timing);
}

.slider-track::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: calc(var(--value, 0) * 1% - 0.5rem);
  background: var(--leftColor);
  border-radius: v-bind(borderRadiusInPx);
  transition: width var(--update);
}

.slider-track::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: calc((100 - var(--value, 0)) * 1% - 0.5rem);
  background: var(--rightColor);
  border-radius: v-bind(borderRadiusInPx);
  transition: width var(--update);
}

.slider-indicator {
  background: var(--indicatorColor);
  left: calc(var(--value, 0) * 1%);
  transition: left var(--update);
}

/* Range input styles */
[type="range"]::-webkit-slider-thumb {
  appearance: none;
  height: 120px;
  width: 40px;
  margin-top: 0px;
  opacity: 1;
}

[type="range"]::-webkit-slider-runnable-track {
  height: 120px;
  background: hsl(10 80% 50% / 0.5);
  margin-top: -60px;
  box-shadow:
    1px 1px 1px #000000,
    0px 0px 1px #0d0d0d;
}

[type="range"]::-moz-range-track {
  height: 120px;
  background: hsl(10 80% 50% / 0.5);
  margin-top: -60px;
  box-shadow:
    1px 1px 1px #000000,
    0px 0px 1px #0d0d0d;
}
</style>

```

## API

| Prop Name        | Type     | Default     | Description                                         |
| ---------------- | -------- | ----------- | --------------------------------------------------- |
| `initialValue`   | `number` | `50`        | Initial position of the slider (0-100).             |
| `leftColor`      | `string` | `"#e68a00"` | Background color for the left side of the slider.   |
| `rightColor`     | `string` | `"#ffffff"` | Background color for the right side of the slider.  |
| `minShiftLimit`  | `number` | `40`        | Minimum limit where shifting animation activates.   |
| `maxShiftLimit`  | `number` | `68`        | Maximum limit where shifting animation deactivates. |
| `leftContent`    | `string` | `"LEFT"`    | Text displayed in the tooltip for the left side.    |
| `rightContent`   | `string` | `"RIGHT"`   | Text displayed in the tooltip for the right side.   |
| `indicatorColor` | `string` | `"#FFFFFF"` | Color of the central indicator on the slider.       |

## Features

- **Dual-Sided Color Control**: Customize the left and right side colors of the slider to create a distinct balance effect.
- **Interactive Tooltip**: Displays real-time percentage values for both sides, with customizable content for left and right labels.
- **Shift Animation**: Activates a shifting effect within defined limits, adding an engaging visual element.
- **Responsive Indicator**: Central indicator adjusts its color based on active state, enhancing interactivity.
- **Accessible Controls**: Works with keyboard and touch interactions for seamless accessibility.

## Credits

- Inspired and ported from code shared in [Jhey's CSS only version of Balance Slider](https://x.com/jh3yy/status/1748809599598399792?s=46)
- Original concept by [Malay Vasa](https://x.com/MalayVasa/status/1748726374079381930).

URL: https://inspira-ui.com/components/miscellaneous/bento-grid

---
title: Bento Grid
description: A cool grid layout with different child component.
---

```vue
<template>
  <BentoGrid class="mx-auto max-w-4xl">
    <BentoGridItem
      v-for="(item, index) in items"
      :key="index"
      :class="index === 3 || index === 6 ? 'md:col-span-2' : ''"
    >
      <template #header>
        <div class="flex size-full animate-pulse space-x-4">
          <div class="flex size-full flex-1 rounded-md bg-zinc-800"></div>
        </div>
      </template>

      <template #title>
        <strong>{{ item.title }}</strong>
      </template>

      <template #icon> </template>

      <template #description>
        <p>{{ item.description }}</p>
      </template>
    </BentoGridItem>
  </BentoGrid>
</template>

<script lang="ts" setup>
const items = [
  {
    title: "The Dawn of Innovation",
    description: "Explore the birth of groundbreaking ideas and inventions.",
  },
  {
    title: "The Digital Revolution",
    description: "Dive into the transformative power of technology.",
  },
  {
    title: "The Art of Design",
    description: "Discover the beauty of thoughtful and experience design.",
  },
  {
    title: "The Power of Communication",
    description: "Understand the impact of effective communication in our lives.",
  },
  {
    title: "The Pursuit of Knowledge",
    description: "Join the quest for understanding and enlightenment.",
  },
  {
    title: "The Joy of Creation",
    description: "Experience the thrill of bringing ideas to life.",
  },
  {
    title: "The Spirit of Adventure",
    description: "Embark on exciting journeys and thrilling discoveries.",
  },
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="bento-grid" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="BentoGrid.vue" language="vue" componentName="BentoGrid" type="ui" id="bento-grid"}
:CodeViewerTab{label="BentoGridCard.vue" language="vue" componentName="BentoGridCard" type="ui" id="bento-grid"}
:CodeViewerTab{label="BentoGridItem.vue" language="vue" componentName="BentoGridItem" type="ui" id="bento-grid"}

::

## Examples

`BentoGrid` in MagicUI style.

```vue
<template>
  <BentoGrid class="grid w-full auto-rows-[22rem] grid-cols-3 gap-4 lg:grid-rows-3">
    <BentoGridCard
      v-for="(feature, index) in features"
      :key="index"
      v-bind="feature"
      :class="feature.class"
    >
      <template
        v-if="feature.image"
        #background
      >
        <div
          class="absolute right-0 top-0 size-full bg-center opacity-40 transition duration-150 ease-in-out group-hover:opacity-20"
          :style="`background-image: url('${feature.image}')`"
        ></div>
      </template>
    </BentoGridCard>
  </BentoGrid>
</template>

<script lang="ts" setup>
import BentoGridCard from "../ui/bento-grid/BentoGridCard.vue";

const features = [
  {
    name: "Save your files",
    description: "We automatically save your files as you type.",
    href: "/",
    image:
      "https://images.pexels.com/photos/2762083/pexels-photo-2762083.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
    cta: "Learn more",
    class: "lg:row-start-1 lg:row-end-4 lg:col-start-2 lg:col-end-3",
  },
  {
    name: "Full text search",
    description: "Search through all your files in one place.",
    href: "/",
    image:
      "https://images.pexels.com/photos/1309766/pexels-photo-1309766.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
    cta: "Learn more",
    class: "lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-3",
  },
  {
    name: "Multilingual",
    description: "Supports 100+ languages and counting.",
    href: "/",
    cta: "Learn more",
    class: "lg:col-start-1 lg:col-end-2 lg:row-start-3 lg:row-end-4",
  },
  {
    name: "Calendar",
    description: "Use the calendar to filter your files by date.",
    href: "/",
    cta: "Learn more",
    class: "lg:col-start-3 lg:col-end-3 lg:row-start-1 lg:row-end-2",
  },
  {
    name: "Notifications",
    description: "Get notified when someone shares a file or mentions you in a comment.",
    href: "/",
    image:
      "https://images.pexels.com/photos/1682821/pexels-photo-1682821.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
    cta: "Learn more",
    class: "lg:col-start-3 lg:col-end-3 lg:row-start-2 lg:row-end-4",
  },
];
</script>

```

## API

#### `BentoGridItem`

| Slot Name     | Description                       |
| ------------- | --------------------------------- |
| `title`       | Component to show as title.       |
| `description` | Component to show as description. |
| `icon`        | Component to show as icon.        |
| `header`      | Component to show as header.      |

#### `BentoGridCard`

| Slot Name    | Description                      |
| ------------ | -------------------------------- |
| `background` | Component to show in background. |

| Props Name    | Type      | Description                          |
| ------------- | --------- | ------------------------------------ |
| `name`        | `string`  | Name or title to show on card.       |
| `icon`        | `?string` | Icon component to show on card.      |
| `description` | `string`  | Description content to show on card. |
| `href`        | `string`  | Link to the url for CTA.             |
| `cta`         | `string`  | Text to show on CTA.                 |

## Credits

- Credits to [Aceternity UI](https://ui.aceternity.com/components/bento-grid) and [Magic UI](https://magicui.design/docs/components/bento-grid) for this fantastic component.

URL: https://inspira-ui.com/components/miscellaneous/book

---
title: Book
description: A 3D book component featuring customizable sizes and color gradients.
---

```vue
<template>
  <div class="grid place-content-center p-10">
    <Book>
      <BookHeader>
        <Icon
          name="heroicons:book-open-solid"
          size="24"
        />
      </BookHeader>
      <BookTitle>
        <h1>The Book</h1>
      </BookTitle>
      <BookDescription>
        <p>Hover me to animate!</p>
      </BookDescription>
    </Book>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="book" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Book.vue" language="vue" componentName="Book" type="ui" id="book"}
:CodeViewerTab{label="BookHeader.vue" language="vue" componentName="BookHeader" type="ui" id="book"}
:CodeViewerTab{label="BookTitle.vue" language="vue" componentName="BookTitle" type="ui" id="book"}
:CodeViewerTab{label="BookDescription.vue" language="vue" componentName="BookDescription" type="ui" id="book"}

```ts [index.ts]
export const BOOK_RADIUS_MAP = {
  sm: "rounded-sm",
  md: "rounded-md",
  lg: "rounded-lg",
  xl: "rounded-xl",
} as const;

export const BOOK_SIZE_MAP = {
  sm: { width: "180px", spineTranslation: "152px" },
  md: { width: "220px", spineTranslation: "192px" },
  lg: { width: "260px", spineTranslation: "232px" },
  xl: { width: "300px", spineTranslation: "272px" },
} as const;

export const BOOK_SHADOW_SIZE_MAP = {
  sm: "-5px 0 15px 5px var(--shadowColor)",
  md: "-7px 0 25px 7px var(--shadowColor)",
  lg: "-10px 0 35px 10px var(--shadowColor)",
  xl: "-12px 0 45px 12px var(--shadowColor)",
} as const;

export const BOOK_COLOR_MAP = {
  slate: { from: "from-slate-900", to: "to-slate-700" },
  gray: { from: "from-gray-900", to: "to-gray-700" },
  zinc: { from: "from-zinc-900", to: "to-zinc-700" },
  neutral: { from: "from-neutral-900", to: "to-neutral-700" },
  stone: { from: "from-stone-900", to: "to-stone-700" },
  red: { from: "from-red-900", to: "to-red-700" },
  orange: { from: "from-orange-900", to: "to-orange-700" },
  amber: { from: "from-amber-900", to: "to-amber-700" },
  yellow: { from: "from-yellow-900", to: "to-yellow-700" },
  lime: { from: "from-lime-900", to: "to-lime-700" },
  green: { from: "from-green-900", to: "to-green-700" },
  emerald: { from: "from-emerald-900", to: "to-emerald-700" },
  teal: { from: "from-teal-900", to: "to-teal-700" },
  cyan: { from: "from-cyan-900", to: "to-cyan-700" },
  sky: { from: "from-sky-900", to: "to-sky-700" },
  blue: { from: "from-blue-900", to: "to-blue-700" },
  indigo: { from: "from-indigo-900", to: "to-indigo-700" },
  violet: { from: "from-violet-900", to: "to-violet-700" },
  purple: { from: "from-purple-900", to: "to-purple-700" },
  fuchsia: { from: "from-fuchsia-900", to: "to-fuchsia-700" },
  pink: { from: "from-pink-900", to: "to-pink-700" },
  rose: { from: "from-rose-900", to: "to-rose-700" },
} as const;

export type BookColor = keyof typeof BOOK_COLOR_MAP;
export type BookSize = keyof typeof BOOK_SIZE_MAP;
export type BookRadius = keyof typeof BOOK_RADIUS_MAP;
export type BookShadowSize = keyof typeof BOOK_SHADOW_SIZE_MAP;

export { default as Book } from "./Book.vue";
export { default as BookHeader } from "./BookHeader.vue";
export { default as BookTitle } from "./BookTitle.vue";
export { default as BookDescription } from "./BookDescription.vue";
```

::

## API

### Components props

::steps{level=4}

#### `Book`

| Prop Name    | Type    | Default | Description                                   |
| ------------ | ------- | ------- | --------------------------------------------- |
| `class`      | String  | -       | Additional classes for styling the component. |
| `duration`   | Number  | 1000    | Animation duration in milliseconds.           |
| `color`      | String  | "zinc"  | Color theme for the book gradient.            |
| `isStatic`   | Boolean | false   | Disables hover animations when true.          |
| `size`       | String  | "md"    | Size variant of the book.                     |
| `radius`     | String  | "md"    | Border radius variant of the book.            |
| `shadowSize` | String  | "lg"    | Shadow size variant of the book.              |

#### `BookHeader`

| Prop Name | Type   | Default | Description                            |
| --------- | ------ | ------- | -------------------------------------- |
| `class`   | String | -       | Additional classes for custom styling. |

#### `BookTitle`

| Prop Name | Type   | Default | Description                            |
| --------- | ------ | ------- | -------------------------------------- |
| `class`   | String | -       | Additional classes for custom styling. |

#### `BookDescription`

| Prop Name | Type   | Default | Description                            |
| --------- | ------ | ------- | -------------------------------------- |
| `class`   | String | -       | Additional classes for custom styling. |

::

## Credits

- Credits to [x/UI](https://ui.3x.gl/docs/book) for inspiring this component.
- Credits to [kalix127](https://github.com/kalix127) for porting this component.

URL: https://inspira-ui.com/components/miscellaneous/compare

---
title: Compare
description: Slide to compare any two pieces of content - images, designs, code, or custom elements
---

```vue
<template>
  <div class="flex w-full justify-center">
    <div
      class="rounded-3xl border border-neutral-200 bg-neutral-100 p-4 dark:border-neutral-800 dark:bg-neutral-900"
    >
      <Compare
        first-image="https://images.vscodethemes.com/antfu.theme-vitesse/vitesse-light-soft-js-preview-4JM.svg"
        second-image="https://images.vscodethemes.com/antfu.theme-vitesse/vitesse-dark-soft-js-preview-4JM.svg"
        first-content-class="object-cover object-left-top rounded-xl overflow-hidden"
        second-content-class="object-cover object-left-top rounded-xl overflow-hidden"
        class="h-[250px] w-[200px] md:h-96 md:w-[500px]"
        slide-mode="hover"
      >
      </Compare>
    </div>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="compare" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Compare.vue" language="vue" componentName="Compare" type="ui" id="compare"}
:CodeViewerTab{label="StarField.vue" language="vue" componentName="StarField" type="ui" id="compare"}

::

## Examples

Drag handle with custom content

```vue
<template>
  <div class="flex w-full justify-center">
    <Compare
      slide-mode="drag"
      class="rounded-xl !bg-zinc-950"
    >
      <template #first-content>
        <div class="size-full overflow-auto bg-zinc-900 p-8">
          <pre
            class="font-mono text-red-400"
            v-text="firstCode"
          ></pre>
        </div>
      </template>
      <template #second-content>
        <div class="size-full overflow-auto bg-zinc-900 p-8">
          <pre
            class="font-mono text-green-400"
            v-text="secondCode"
          ></pre>
        </div>
      </template>
    </Compare>
  </div>
</template>

<script setup lang="ts">
import { firstCode, secondCode } from "./code/index";
</script>

```

AutoPlay

```vue
<template>
  <div
    class="flex h-[60vh] w-full items-center justify-center px-1 [perspective:800px] [transform-style:preserve-3d] md:px-8"
  >
    <div
      :style="{
        transform: 'rotateX(15deg) translateZ(80px)',
      }"
      class="mx-auto h-1/2 w-3/4 rounded-3xl border border-neutral-200 bg-neutral-100 p-1 md:h-3/4 md:p-4 dark:border-neutral-800 dark:bg-neutral-900"
    >
      <Compare
        first-image="/images/Inspira-light.png"
        second-image="/images/Inspira-dark.png"
        first-content-class="object-cover object-left-center w-full h-96"
        second-content-class="object-cover object-left-center w-full h-96"
        class="size-full rounded-[22px] md:rounded-xl"
        slide-mode="hover"
        :autoplay="true"
      />
    </div>
  </div>
</template>

```

Custom Content with AutoPlay

```vue
<template>
  <div class="flex w-full justify-center">
    <Compare
      class="rounded-xl shadow-lg"
      :autoplay="true"
      :autoplay-duration="3000"
      slide-mode="hover"
    >
      <template #first-content>
        <div class="flex size-full items-center justify-center bg-blue-500">
          <div class="text-4xl font-bold text-white">☀️ Day</div>
        </div>
      </template>
      <template #second-content>
        <div class="flex size-full items-center justify-center bg-gray-900">
          <div class="text-4xl font-bold text-white">🌙 Night</div>
        </div>
      </template>
    </Compare>
  </div>
</template>

```

## API

### Props

| Prop Name                 | Type                | Default          | Description                               |
| ------------------------- | ------------------- | ---------------- | ----------------------------------------- |
| `firstImage`              | `string`            | `""`             | URL of the first image                    |
| `secondImage`             | `string`            | `""`             | URL of the second image                   |
| `firstImageAlt`           | `string`            | `"First image"`  | Alt text for first image                  |
| `secondImageAlt`          | `string`            | `"Second image"` | Alt text for second image                 |
| `class`                   | `string`            | `""`             | Additional classes for the component      |
| `firstContentClass`       | `string`            | `""`             | Classes applied to first content wrapper  |
| `secondContentClass`      | `string`            | `""`             | Classes applied to second content wrapper |
| `initialSliderPercentage` | `number`            | `50`             | Initial position of slider (0-100)        |
| `slideMode`               | `"hover" \| "drag"` | `"hover"`        | Interaction mode for the slider           |
| `showHandlebar`           | `boolean`           | `true`           | Show/hide the handle bar                  |
| `autoplay`                | `boolean`           | `false`          | Enable/disable autoplay                   |
| `autoplayDuration`        | `number`            | `5000`           | Duration of autoplay cycle in ms          |

### Events

| Event Name          | Payload  | Description                                  |
| ------------------- | -------- | -------------------------------------------- |
| `update:percentage` | `number` | Emitted when slider position changes (0-100) |
| `drag:start`        | -        | Emitted when dragging starts                 |
| `drag:end`          | -        | Emitted when dragging ends                   |
| `hover:enter`       | -        | Emitted when mouse enters component          |
| `hover:leave`       | -        | Emitted when mouse leaves component          |

### Slots

| Slot Name        | Default Content                                   | Description                                                                                                                       |
| ---------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `first-content`  | `<img>` element if `firstImage` prop is provided  | Content shown on the left/first side of the comparison. Has full access to component width/height.                                |
| `second-content` | `<img>` element if `secondImage` prop is provided | Content shown on the right/second side of the comparison. Has full access to component width/height.                              |
| `handle`         | Default slider handle with dots icon              | Custom handle for the slider. Automatically positioned at the dividing line. Should handle positioning with absolute positioning. |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for this component.

- Inspired from [Aceternity UI's Compare](https://ui.aceternity.com/components/compare).

URL: https://inspira-ui.com/components/miscellaneous/container-scroll

---
title: Container Scroll
description: A container scrolling effect that transforms the content inside based on scroll progress. Features smooth transitions with scaling and rotating effects on scroll.
---

```vue
<template>
  <div class="flex flex-col overflow-hidden">
    <ContainerScroll>
      <template #title>
        <h1 class="text-4xl font-semibold text-black dark:text-white">
          Unleash the power of <br />
          <span class="mt-1 text-4xl font-bold leading-none md:text-[6rem]">
            Scroll Animations
          </span>
        </h1>
      </template>
      <template #card>
        <img
          src="/linear.webp"
          class="mx-auto h-full rounded-2xl object-cover object-left-top"
          alt="hero"
          height="720"
          width="1400"
        />
      </template>
    </ContainerScroll>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="container-scroll" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="ContainerScroll.vue" language="vue" componentName="ContainerScroll" type="ui" id="container-scroll"}
:CodeViewerTab{label="ContainerScrollTitle.vue" language="vue" componentName="ContainerScrollTitle" type="ui" id="container-scroll"}
:CodeViewerTab{label="ContainerScrollCard.vue" language="vue" componentName="ContainerScrollCard" type="ui" id="container-scroll"}

::

## API

::steps

### `ContainerScroll`

The `ContainerScroll` component creates a 3D scroll effect. As the user scrolls, the content inside the container is transformed with scale, rotation, and translation effects.

#### Props

| Prop Name    | Type   | Default | Description                                                                   |
| ------------ | ------ | ------- | ----------------------------------------------------------------------------- |
| `rotate`     | Number | `0`     | Controls the rotation of the inner content based on the scroll progress.      |
| `scale`      | Number | `1`     | Controls the scaling transformation applied to the content during the scroll. |
| `translateY` | Number | `0`     | Controls the vertical translation of the title during the scroll.             |

#### Usage

```vue [ContainerScroll.vue]
<ContainerScroll :rotate="rotateValue" :scale="scaleValue" :translateY="translateYValue">
  <template #title>
    <!-- Your title content here -->
  </template>
  <template #card>
    <!-- Your card content here -->
  </template>
</ContainerScroll>
```

### `ContainerScrollTitle`

The `ContainerScrollTitle` component handles the title's transformation as the user scrolls, applying a vertical translation effect.

#### Props

| Prop Name   | Type   | Default | Description                                     |
| ----------- | ------ | ------- | ----------------------------------------------- |
| `translate` | Number | `0`     | Controls the vertical translation of the title. |

#### Usage

```vue [ContainerScrollTitle.vue]
<ContainerScrollTitle :translate="translateYValue">
  <!-- Title content here -->
</ContainerScrollTitle>
```

### `ContainerScrollCard`

The `ContainerScrollCard` component applies scale and rotation effects to the card content as the user scrolls through the page.

#### Props

| Prop Name | Type   | Default | Description                                      |
| --------- | ------ | ------- | ------------------------------------------------ |
| `rotate`  | Number | `0`     | Controls the rotation effect of the card.        |
| `scale`   | Number | `1`     | Controls the scaling effect applied to the card. |

#### Usage

```vue [ContainerScrollCard.vue]
<ContainerScrollCard :rotate="rotateValue" :scale="scaleValue">
  <!-- Card content here -->
</ContainerScrollCard>
```

::

## Features

- **3D Scroll Effects**: The content transforms based on scroll progress, including rotation, scaling, and translation.
- **Responsive Design**: Adjusts behavior dynamically for mobile and desktop views.
- **Smooth Transitions**: Leverages CSS keyframes and scroll-based transformations for a fluid user experience.

## CSS Variables

To customize the scroll animations and responsiveness, you can set the following CSS variables:

- **`--scale-start`**: Initial scale value for the card.
- **`--scale-end`**: Final scale value for the card as the scroll progresses.
- **`--rotate-start`**: Initial rotation value for the card.
- **`--rotate-end`**: Final rotation value for the card as the scroll progresses.

## Features

- **Dynamic Transformations**: Based on scroll position, the content scales, rotates, and translates for a 3D effect.
- **Flexible Content**: Place any custom content inside the title and card slots.
- **Responsive**: Adjusts for mobile and desktop, providing a consistent experience across devices.

## Credits

- Inspired by [Aceternity UI Container Scroll Animation](https://ui.aceternity.com/components/container-scroll-animation).

URL: https://inspira-ui.com/components/miscellaneous/dock

---
title: Dock
description: A macOS-style dock with magnifying icons as you hover over them.
---

```vue
<template>
  <Dock class="mb-6">
    <DockIcon>
      <Icon
        name="mdi:github"
        class="size-full"
      />
    </DockIcon>
    <DockSeparator />
    <DockIcon>
      <Icon
        name="logos:google-drive"
        class="size-full"
      />
    </DockIcon>
    <DockIcon>
      <Icon
        name="logos:notion-icon"
        class="size-full"
      />
    </DockIcon>
    <DockIcon>
      <Icon
        name="logos:whatsapp-icon"
        class="size-full"
      />
    </DockIcon>
  </Dock>
</template>

```

## Examples

Vertical Dock

```vue
<template>
  <Dock orientation="vertical">
    <DockIcon>
      <Icon
        name="mdi:github"
        class="size-full"
      />
    </DockIcon>
    <DockSeparator />
    <DockIcon>
      <Icon
        name="logos:google-drive"
        class="size-full"
      />
    </DockIcon>
    <DockIcon>
      <Icon
        name="logos:notion-icon"
        class="size-full"
      />
    </DockIcon>
    <DockIcon>
      <Icon
        name="logos:whatsapp-icon"
        class="size-full"
      />
    </DockIcon>
  </Dock>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="dock" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Dock.vue" language="vue" componentName="Dock" type="ui" id="dock"}
:CodeViewerTab{label="DockIcon.vue" language="vue" componentName="DockIcon" type="ui" id="dock"}

::

## API

::steps

### `Dock`

| Prop Name       | Type     | Description                                                            |
| --------------- | -------- | ---------------------------------------------------------------------- |
| `class`         | `string` | Additional classes to apply to the dock container.                     |
| `magnification` | `number` | Magnification factor for the dock icons on hover (default: 60).        |
| `distance`      | `number` | Distance from the icon center where magnification applies.             |
| `direction`     | `string` | Alignment of icons (`top`, `middle`, `bottom`) (default: middle).      |
| `orientation`   | `string` | Orientation of Dock (`'vertical`, `horizontal`) (default: horizontal). |

| Slot Name | Description                                          |
| --------- | ---------------------------------------------------- |
| `default` | Dock Dock or other child components to be displayed. |

### `DockIcon`

| Slot Name | Description                                             |
| --------- | ------------------------------------------------------- |
| `default` | Component or icon to be displayed inside the dock icon. |

::

## Credits

- Credits to macOS Dock for the design inspiration and the fantastic hover magnification effect.

URL: https://inspira-ui.com/components/miscellaneous/expandable-gallery

---
title: Expandable Gallery
description: A responsive image gallery with an interactive hover effect that expands images dynamically.
---

```vue
<template>
  <ExpandableGallery
    :images="images"
    class="p-4"
  />
</template>

<script lang="ts" setup>
const images = [
  "https://images.unsplash.com/photo-1709884735646-897b57461d61?q=80&w=3628&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  "https://images.unsplash.com/photo-1541701494587-cb58502866ab?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  "https://images.unsplash.com/photo-1527529482837-4698179dc6ce?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  "https://images.unsplash.com/photo-1502085671122-2d218cd434e6?q=80&w=3626&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="expandable-gallery" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div :class="cn('flex h-96 w-full gap-2', props.class)">
    <div
      v-for="image in images"
      :key="image"
      class="relative flex h-full flex-1 cursor-pointer overflow-hidden rounded-xl transition-all duration-500 ease-in-out hover:flex-[3]"
    >
      <img
        class="relative h-full object-cover"
        :src="image"
        :alt="image"
      />
    </div>
  </div>
</template>

<script lang="ts" setup>
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

interface Props {
  images: string[];
  class?: HTMLAttributes["class"];
}

const props = defineProps<Props>();
</script>

```

## API

| Prop Name | Type       | Default | Description                                    |
| --------- | ---------- | ------- | ---------------------------------------------- |
| `images`  | `string[]` | `[]`    | Array of image URLs to display in the gallery. |

## Features

- **Interactive Hover Effect**: Images expand when hovered over, creating a dynamic and engaging user experience.
- **Responsive Design**: The gallery automatically adjusts to the container size, ensuring it looks great on all devices.
- **Smooth Transitions**: Includes smooth scaling animations for a polished visual effect.
- **Customizable Content**: Easily update the `images` array to change the gallery's content.

## Credits

- Inspired from [Expandable Gallery Component by David Mráz](https://x.com/davidm_ml/status/1872319793124282653)

URL: https://inspira-ui.com/components/miscellaneous/images-slider

---
title: Images Slider
description: A full page slider with images that can be navigated with the keyboard.
---

```vue
<template>
  <div>
    <ImagesSlider
      :images="images"
      autoplay
    >
      <div
        class="flex size-full items-end justify-center bg-gradient-to-b from-transparent via-transparent to-black/80 p-8"
      >
        <div class="max-w-prose text-center">
          <h1 class="mb-8 text-5xl max-md:text-2xl">
            The hero section slide-show nobody asked for
          </h1>
          <button
            class="rounded-full border border-emerald-500/20 bg-emerald-300/10 px-4 py-2 text-sm text-white backdrop-blur-sm"
          >
            Join Now
          </button>
        </div>
      </div>
    </ImagesSlider>
  </div>
</template>

<script lang="ts" setup>
const images = [
  "https://picsum.photos/seed/waiting/1920/1080",
  "https://picsum.photos/seed/full-collapse/1920/1080",
  "https://picsum.photos/seed/five-stories-falling/1920/1080",
  "https://picsum.photos/seed/war-all-the-time/1920/1080",
  "https://picsum.photos/seed/a-city-by-the-light-divided/1920/1080",
  "https://picsum.photos/seed/common-existence/1920/1080",
  "https://picsum.photos/seed/no-devolucion/1920/1080",
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="images-slider" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="sliderRef"
    tabindex="0"
    class="relative flex size-full items-center justify-center overflow-hidden transition-colors focus:outline-none focus:ring-1"
    :style="{
      perspective: props.perspective,
    }"
  >
    <Transition
      mode="out-in"
      v-bind="transitionProps"
    >
      <div
        :key="currentImage"
        class=""
      >
        <img
          :src="currentImage"
          :class="props.imageClass"
        />
      </div>
    </Transition>
    <div
      v-if="hideOverlay !== true"
      :class="cn('absolute inset-0', props.overlayClass)"
    >
      <Transition
        appear
        enter-active-class="transition-all duration-300 delay-300 ease-in-out"
        enter-from-class="opacity-0 -translate-y-10"
      >
        <slot
          v-if="!isLoading"
          :current-index="currentIndex"
        ></slot>
      </Transition>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useIntervalFn, onKeyStroke, useSwipe } from "@vueuse/core";
import { ref, watch, computed, type PropType, type Ref } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps({
  images: {
    type: Array as PropType<string[]>,
    default: () => [],
  },
  hideOverlay: {
    type: Boolean,
    default: false,
  },
  overlayClass: {
    type: String,
    default: "",
  },
  imageClass: {
    type: String,
    default: "bg-cover bg-center bg-no-repeat",
  },
  enterFromClass: {
    type: String,
    default: "scale-0 origin-center",
  },
  enterActiveClass: {
    type: String,
    default: "transition-transform duration-300 ease-in-out",
  },
  leaveActiveClass: {
    type: String,
    default: "transition-transform duration-300 ease-in-out",
  },
  autoplay: {
    type: [Boolean, Number, String],
    default: false,
  },
  direction: {
    type: String,
    default: "vertical",
    validator: (v: string) => ["vertical", "horizontal"].includes(v),
  },
  perspective: {
    type: String,
    default: "1000px",
  },
});

const sliderRef = ref(null);
const currentDirection = ref("up");
const currentIndex = defineModel("modelValue", {
  type: Number,
  default: 0,
});

function setCurrentDirection(dir: "prev" | "next") {
  if (props.direction === "horizontal") {
    currentDirection.value = dir === "next" ? "left" : "right";
  } else {
    currentDirection.value = dir === "next" ? "up" : "down";
  }
}

// Image Loading
const isLoading = ref(true);
const isTransitioning = ref(false);
const loadedImages: Ref<string[]> = ref([]);
const currentImage = computed(() => loadedImages.value[currentIndex.value]);

function loadImages() {
  isLoading.value = true;
  const promises = props.images.map(
    (imageSrc): Promise<string> =>
      new Promise((resolve, reject) => {
        const image = new Image();
        image.src = imageSrc;
        image.onload = () => resolve(imageSrc);
        image.onerror = () => reject(imageSrc);
      }),
  );
  Promise.all(promises).then((resolvedImages) => {
    loadedImages.value = resolvedImages;
    isLoading.value = false;
  });
}
loadImages();

// Navigation

function onPrev() {
  if (isLoading.value || isTransitioning.value) {
    return false;
  }
  setCurrentDirection("prev");
  let target = currentIndex.value - 1;
  if (target < 0) {
    target = loadedImages.value.length - 1;
  }
  currentIndex.value = target;
}

function onNext() {
  if (isLoading.value || isTransitioning.value) {
    return false;
  }
  setCurrentDirection("next");
  let target = currentIndex.value + 1;
  if (target >= loadedImages.value?.length - 1) {
    target = 0;
  }
  currentIndex.value = target;
}

onKeyStroke(
  ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"],
  (ev) => {
    ev.preventDefault();
    if (isLoading.value || isTransitioning.value) {
      return false;
    }
    pause();
    if (["ArrowUp", "ArrowLeft"].includes(ev.key)) {
      onPrev();
    } else {
      onNext();
    }
  },
  {
    target: sliderRef,
  },
);

const { direction: swipingDirection, isSwiping } = useSwipe(sliderRef, {
  passive: false,
});
watch(swipingDirection, (dir) => {
  switch (dir) {
    case "up":
    case "left":
      return onPrev();
    case "down":
    case "right":
      return onNext();
  }
});

watch(isSwiping, setSwiping);

function setSwiping(v: boolean) {
  if (v) {
    pause();
    return;
  }

  resume();
}

// Autoplay
const autoplayInterval = computed(() => {
  if (props.autoplay === false) return 0;
  else if (props.autoplay === true) return 5000;
  else if (typeof props.autoplay === "string") {
    return +props.autoplay;
  } else return props.autoplay;
});

const { pause, resume, isActive } = useIntervalFn(() => {
  onNext();
}, autoplayInterval);

watch(isLoading, (v) => {
  if (v) pause();
  else resume();
});

// Transitions
function lockViewport() {
  isTransitioning.value = true;
}

function unlockViewport() {
  isTransitioning.value = false;
  if (isActive.value === false && autoplayInterval.value) {
    resume();
  }
}
const transitionProps = computed(() => {
  const bind = {
    enterActiveClass: props.enterActiveClass,
    leaveActiveClass: props.leaveActiveClass,
    enterFromClass: props.enterFromClass,
    leaveToClass: "",
    onBeforeLeave: lockViewport,
    onAfterEnter: unlockViewport,
  };

  switch (currentDirection.value) {
    case "up":
      bind.leaveToClass = "-translate-y-full";
      break;
    case "down":
      bind.leaveToClass = "translate-y-full";
      break;
    case "left":
      bind.leaveToClass = "-translate-x-full";
      break;
    case "right":
      bind.leaveToClass = "translate-x-full";
      break;
  }

  return bind;
});
</script>

```

## API

| Prop Name          | Type                       | Default                                           | Description                                                                    |
| ------------------ | -------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ |
| `images`           | `string[]`                 | `[]`                                              | An array of image URLs to show in the slider.                                  |
| `hideOverlay`      | `boolean`                  | `false`                                           | Don't create an overlay for the image slider. Slot won't be rendered.          |
| `overlayClass`     | `string`                   | `''`                                              | A class string to be applied to the overlay container.                         |
| `imageClass`       | `string`                   | `'bg-cover bg-center bg-no-repeat'`               | Class string to apply to each of the images.                                   |
| `enterFromClass`   | `string`                   | `'scale-0 origin-center'`                         | Class string applied to the 'enter-from-class' prop on the image transition.   |
| `enterActiveClass` | `string`                   | `'transition-transform duration-300 ease-in-out'` | Class string applied to the 'enter-active-class' prop on the image transition. |
| `leaveActiveClass` | `string`                   | `'transition-transform duration-300 ease-in-out'` | Class string applied to the 'leave-active-class' prop on the image transition. |
| `autoplay`         | `boolean\|number`          | `false`                                           | Autoplay interval in ms, or `false` to disable.                                |
| `direction`        | `'vertical'\|'horizontal'` | `'vertical'`                                      | The axis on which the slider should operate.                                   |
| `perspective`      | `string`                   | `'1000px'`                                        | The perspective to apply to the slider container.                              |
| `modelValue`       | `number`                   | `0`                                               | Two-way binding for the current slide image index.                             |

## Features

- **Horizontal & Vertical Animations**: You can animate the images horizontally (default) or vertically with the `direction` prop.
- **Preloaded Images**: Images are preloaded before showing, preventing flickering loading animation.
- **Customisable Autoplay**: Automatically transition through your slides, or allow your users to navigate manually.
- **Overlay Anything**: The default slot allows you to overlay whatever content you wish overlay slider.

## Credits

- Component by [Craig Riley](https://github.com/craigrileyuk) for porting this component.
- Credits to [Aceternity UI](https://ui.aceternity.com/components/images-slider) for inspiring this component.

URL: https://inspira-ui.com/components/miscellaneous/lens

---
title: Lens
description: A lens component to zoom into images, videos, or practically anything.
---

```vue
<template>
  <div
    class="relative mx-auto my-10 w-full max-w-md overflow-hidden rounded-3xl bg-gradient-to-r from-[#1D2235] to-[#121318] p-8"
  >
    <Rays />
    <Beams />
    <div class="relative z-10">
      <Lens
        :hovering="hovering"
        @hover-update="setHovering"
      >
        <img
          src="https://images.unsplash.com/photo-1713869820987-519844949a8a?q=80&w=3500&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
          alt="image"
          width="500"
          height="500"
          class="rounded-2xl"
        />
      </Lens>
      <div
        :style="{ filter: hovering ? 'blur(2px)' : 'blur(0px)' }"
        class="relative z-20 py-4"
      >
        <h2 class="text-left text-2xl font-bold text-white">Apple Vision Pro</h2>
        <p class="mt-4 text-left text-neutral-200">
          The all new apple vision pro was the best thing that happened around 8 months ago, not
          anymore.
        </p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const hovering = ref(false);

function setHovering(value: boolean) {
  hovering.value = value;
}
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="lens" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Lens.vue" language="vue" componentName="Lens" type="ui" id="lens"}
:CodeViewerTab{label="Rays.vue" language="vue" componentName="Rays" type="examples" id="lens"}
:CodeViewerTab{label="Beams.vue" language="vue" componentName="Beams" type="examples" id="lens"}

::

## Examples

Lens are static in center

```vue
<template>
  <div
    class="relative mx-auto my-10 w-full max-w-md overflow-hidden rounded-3xl bg-gradient-to-r from-[#1D2235] to-[#121318] p-8"
  >
    <Rays />
    <Beams />
    <div class="relative z-10">
      <Lens is-static>
        <img
          src="https://images.unsplash.com/photo-1713869820987-519844949a8a?q=80&w=3500&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
          alt="image"
          width="500"
          height="500"
          class="rounded-2xl"
        />
      </Lens>
      <div
        :style="{ filter: hovering ? 'blur(2px)' : 'blur(0px)' }"
        class="relative z-20 py-4"
      >
        <h2 class="text-left text-2xl font-bold text-white">Apple Vision Pro</h2>
        <p class="mt-4 text-left text-neutral-200">
          The all new apple vision pro was the best thing that happened around 8 months ago, not
          anymore.
        </p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const hovering = ref(false);

function setHovering(value: boolean) {
  hovering.value = value;
}
</script>

```

## API

| Prop Name    | Type                       | Default              | Description                                                                  |
| ------------ | -------------------------- | -------------------- | ---------------------------------------------------------------------------- |
| `zoomFactor` | `number`                   | `1.5`                | The magnification factor for the lens.                                       |
| `lensSize`   | `number`                   | `170`                | The diameter of the lens in pixels.                                          |
| `position`   | `{ x: number, y: number }` | `{ x: 200, y: 150 }` | The static position of the lens (when isStatic is true).                     |
| `isStatic`   | `boolean`                  | `false`              | If true, the lens stays in a fixed position; if false, it follows the mouse. |
| `hovering`   | `boolean`                  | `"false"`            | External control for the hover state.                                        |

## Features

- **Slot Support**: Easily add any content inside the component using the default slot.

## Credits

- Credits to [Aceternity UI](https://ui.aceternity.com/components/lens).
- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for porting this component.

URL: https://inspira-ui.com/components/miscellaneous/link-preview

---
title: Link Preview
description: Dynamic link previews for your anchor tags
---

```vue
<template>
  <div class="flex h-[40rem] flex-col items-center justify-center px-4">
    <p class="mx-auto mb-10 max-w-3xl text-xl text-neutral-500 md:text-3xl dark:text-neutral-400">
      <LinkPreview
        url="https://tailwindcss.com"
        class="font-bold"
      >
        Tailwind CSS
      </LinkPreview>
      and
      <LinkPreview
        url="https://motion.unovue.com/"
        class="font-bold"
      >
        motion-v
      </LinkPreview>
      are a great way to build modern websites.
    </p>
    <p class="mx-auto max-w-3xl text-xl text-neutral-500 md:text-3xl dark:text-neutral-400">
      Visit
      <LinkPreview
        url="https://inspira-ui.com"
        :width="400"
        :height="200"
      >
        <span
          class="bg-gradient-to-br from-purple-500 to-pink-500 bg-clip-text font-bold text-transparent"
        >
          Inspira UI
        </span>
      </LinkPreview>
      for more cool components
    </p>
  </div>
</template>

```

::alert{type="info"}
**Note:** This component uses `qss` npm package as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="link-preview" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install qss
```

```bash [pnpm]
pnpm install qss
```

```bash [bun]
bun add qss
```

```bash [yarn]
yarn add qss
```

::

Copy and paste the following code

```vue
<template>
  <div :class="cn('relative inline-block', props.class)">
    <!-- Trigger -->
    <NuxtLink
      :to="url"
      :class="cn('text-black dark:text-white', props.linkClass)"
      @mousemove="handleMouseMove"
      @mouseenter="showPreview"
      @mouseleave="hidePreview"
    >
      <slot />
    </NuxtLink>

    <!-- Preview -->
    <div
      v-if="isVisible"
      ref="preview"
      class="pointer-events-none absolute z-50"
      :style="previewStyle"
    >
      <div
        class="overflow-hidden rounded-xl shadow-xl"
        :class="[popClass, { 'transform-gpu': !props.isStatic }]"
      >
        <div
          class="block rounded-xl border-2 border-transparent bg-white p-1 shadow-lg dark:bg-gray-900"
        >
          <img
            :src="previewSrc"
            :width="width"
            :height="height"
            class="size-full rounded-lg object-cover"
            :style="imageStyle"
            alt="preview"
            @load="handleImageLoad"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, reactive, type CSSProperties } from "vue";
import { cn } from "@/lib/utils";

interface BaseProps {
  class?: string;
  linkClass?: string;
  width?: number;
  height?: number;
}

// Props for static image mode
interface StaticImageProps extends BaseProps {
  isStatic: true;
  imageSrc: string;
  url?: string; // optional in static mode
}

// Props for URL preview mode
interface URLPreviewProps extends BaseProps {
  isStatic?: false; // optional but must be false if specified
  imageSrc?: string; // optional in URL mode
  url: string;
}

// Combined type that enforces the requirements
type Props = StaticImageProps | URLPreviewProps;
const props = withDefaults(defineProps<Props>(), {
  isStatic: false,
  imageSrc: "",
  url: "",
  width: 200,
  height: 125,
});

const isVisible = ref(false);
const isLoading = ref(true);
const preview = ref<HTMLElement | null>(null);
const hasPopped = ref(false);

// Generate preview URL
const previewSrc = computed(() => {
  if (props.isStatic) return props.imageSrc;

  const params = new URLSearchParams({
    url: props.url,
    screenshot: "true",
    meta: "false",
    embed: "screenshot.url",
    colorScheme: "light",
    "viewport.isMobile": "true",
    "viewport.deviceScaleFactor": "1",
    "viewport.width": String(props.width * 3),
    "viewport.height": String(props.height * 3),
  });

  return `https://api.microlink.io/?${params.toString()}`;
});

// Position tracking
const mousePosition = reactive({
  x: 0,
  y: 0,
});

// Calculate preview position
const previewStyle = computed<CSSProperties>(() => {
  if (!preview.value) return {};

  const offset = 20;
  const previewWidth = props.width;
  const previewHeight = props.height;
  const viewportWidth = window.innerWidth;

  let x = mousePosition.x - previewWidth / 2;
  x = Math.min(Math.max(0, x), viewportWidth - previewWidth);

  const linkRect = preview.value.parentElement?.getBoundingClientRect();
  const y = linkRect ? linkRect.top - previewHeight - offset : 0;

  return {
    position: "fixed",
    left: `${x}px`,
    top: `${y}px`,
    width: `${previewWidth}px`,
    height: `${previewHeight}px`,
  };
});

// Image specific styling
const imageStyle = computed<CSSProperties>(() => ({
  width: `${props.width}px`,
  height: `${props.height}px`,
}));

// Pop animation class
const popClass = computed(() => {
  if (!hasPopped.value) return "";
  return "animate-pop";
});

function handleMouseMove(event: MouseEvent) {
  mousePosition.x = event.clientX;
  mousePosition.y = event.clientY;
}

function showPreview() {
  isVisible.value = true;
  setTimeout(() => {
    hasPopped.value = true;
  }, 50);
}

function hidePreview() {
  isVisible.value = false;
  hasPopped.value = false;
}

function handleImageLoad() {
  isLoading.value = false;
}
</script>

<style scoped>
.transform-gpu {
  transform: scale3d(0, 0, 1);
  transform-origin: center bottom;
  will-change: transform;
  backface-visibility: hidden;
}

.animate-pop {
  animation: pop 1000ms ease forwards;
  will-change: transform;
}

@keyframes pop {
  0% {
    transform: scale3d(0.26, 0.26, 1);
  }
  25% {
    transform: scale3d(1.1, 1.1, 1);
  }
  65% {
    transform: scale3d(0.98, 0.98, 1);
  }
  100% {
    transform: scale3d(1, 1, 1);
  }
}
</style>

```
::

## API

| Prop Name   | Type      | Default | Description                                                                                 |
| ----------- | --------- | ------- | ------------------------------------------------------------------------------------------- |
| `class`     | `string`  | `""`    | Custom class applied to the main element.                                                   |
| `linkClass` | `string`  | `""`    | Custom class applied to the link element.                                                   |
| `width`     | `number`  | `200`   | Width of the preview image.                                                                 |
| `height`    | `number`  | `125`   | Height of the preview image.                                                                |
| `isStatic`  | `boolean` | `false` | Determines if the preview image is static or a URL preview (set to `true` for static mode). |
| `imageSrc`  | `string`  | `""`    | The source of the image to display (required if `isStatic` is `true`).                      |
| `url`       | `string`  | `""`    | URL for the link and for generating the preview image (required if `isStatic` is `false`).  |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for porting this component.

- Ported from [Aceternity UI's Link Preview](https://ui.aceternity.com/components/link-preview).

URL: https://inspira-ui.com/components/miscellaneous/marquee

---
title: Marquee
description: A customizable scrolling component that loops its content horizontally or vertically, with configurable direction, hover pause, and repeat options.
---

```vue
<template>
  <div
    class="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
  >
    <!-- First Marquee -->
    <Marquee
      pause-on-hover
      class="[--duration:20s]"
    >
      <ReviewCard
        v-for="review in firstRow"
        :key="review.username"
        :img="review.img"
        :name="review.name"
        :username="review.username"
        :body="review.body"
      />
    </Marquee>

    <!-- Second Marquee (reverse) -->
    <Marquee
      reverse
      pause-on-hover
      class="[--duration:20s]"
    >
      <ReviewCard
        v-for="review in secondRow"
        :key="review.username"
        :img="review.img"
        :name="review.name"
        :username="review.username"
        :body="review.body"
      />
    </Marquee>

    <!-- Left Gradient -->
    <div
      class="pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-white dark:from-background"
    ></div>

    <!-- Right Gradient -->
    <div
      class="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-gradient-to-l from-white dark:from-background"
    ></div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

// Reviews data
const reviews = [
  {
    name: "Jack",
    username: "@jack",
    body: "I've never seen anything like this before. It's amazing. I love it.",
    img: "https://avatar.vercel.sh/jack",
  },
  {
    name: "Jill",
    username: "@jill",
    body: "I don't know what to say. I'm speechless. This is amazing.",
    img: "https://avatar.vercel.sh/jill",
  },
  {
    name: "John",
    username: "@john",
    body: "I'm at a loss for words. This is amazing. I love it.",
    img: "https://avatar.vercel.sh/john",
  },
  {
    name: "Jane",
    username: "@jane",
    body: "I'm at a loss for words. This is amazing. I love it.",
    img: "https://avatar.vercel.sh/jane",
  },
  {
    name: "Jenny",
    username: "@jenny",
    body: "I'm at a loss for words. This is amazing. I love it.",
    img: "https://avatar.vercel.sh/jenny",
  },
  {
    name: "James",
    username: "@james",
    body: "I'm at a loss for words. This is amazing. I love it.",
    img: "https://avatar.vercel.sh/james",
  },
];

// Split reviews into two rows
const firstRow = ref(reviews.slice(0, reviews.length / 2));
const secondRow = ref(reviews.slice(reviews.length / 2));
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="marquee" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Marquee.vue" language="vue" componentName="Marquee" type="ui" id="marquee"}
:CodeViewerTab{label="ReviewCard.vue" language="vue" componentName="ReviewCard" type="examples" id="marquee"}

::

## API

| Prop Name      | Type      | Default | Description                                                               |
| -------------- | --------- | ------- | ------------------------------------------------------------------------- |
| `class`        | `string`  | `''`    | Custom CSS classes to apply to the outermost container of the marquee.    |
| `reverse`      | `boolean` | `false` | Sets the scrolling direction to reverse (right to left or bottom to top). |
| `pauseOnHover` | `boolean` | `false` | Pauses the marquee animation when hovered.                                |
| `vertical`     | `boolean` | `false` | Sets the scrolling direction to vertical instead of horizontal.           |
| `repeat`       | `number`  | `4`     | Number of times the content inside the marquee should be repeated.        |

## Features

- **Horizontal & Vertical Scrolling**: You can scroll the content horizontally (default) or vertically with the `vertical` prop.
- **Reverse Direction**: Enable reverse scrolling for dynamic effects using the `reverse` prop.
- **Pause on Hover**: Pauses the scrolling animation when the user hovers over the marquee.
- **Content Repetition**: The `repeat` prop controls how many times the content inside the marquee will loop.
- **Custom Duration and Gap**: You can control the animation duration and gap between the repeated items using CSS variables `--duration` and `--gap`.

## CSS Variables

You can customize the speed and gap between the items by setting the following CSS variables:

- **`--duration`**: Controls the speed of the marquee animation.
- **`--gap`**: Sets the space between repeated items in the marquee.

## Features

- **Fully Customizable**: Easily adjust the duration, gap, direction, and hover behavior to suit your needs.
- **Smooth Transitions**: The component uses CSS keyframes to achieve smooth scrolling and transitions.
- **Multi-Directional**: Scroll content either horizontally or vertically with a simple prop change.
- **Content Flexibility**: Place any Vue components or HTML elements inside the marquee slot for dynamic content scrolling.

## Credits

- Inspired by [Magic UI](https://magicui.design/docs/components/marquee).

URL: https://inspira-ui.com/components/miscellaneous/morphing-tabs

---
title: Morphing Tabs
description: This is a morphing tabs interaction, recreated by Preet's work and featuring the gooey effect component.
---

```vue
<template>
  <MorphingTabs
    :tabs="tabs"
    :active-tab="activeTab"
    @update:active-tab="activeTab = $event"
  />
</template>

<script lang="ts" setup>
import { ref } from "vue";

const tabs = ["Home", "Apps", "About", "My"];
const activeTab = ref(tabs[0]);
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="morphing-tabs" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    v-if="props.tabs.length"
    :class="cn('relative', props.class)"
    style="filter: url(&quot;#exclusionTabsGoo&quot;)"
  >
    <button
      v-for="tab in props.tabs"
      :key="tab"
      :class="cn('px-4 py-2 bg-primary text-background transition-all duration-500')"
      :style="{
        margin: `0 ${activeTab === tab ? props.margin : 0}px`,
      }"
      @click="emit('update:activeTab', tab)"
    >
      {{ tab }}
    </button>

    <div class="absolute w-full">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        version="1.1"
      >
        <defs>
          <filter
            id="exclusionTabsGoo"
            x="-50%"
            y="-50%"
            width="200%"
            height="200%"
            color-interpolation-filters="sRGB"
          >
            <feGaussianBlur
              in="SourceGraphic"
              :stdDeviation="blurStdDeviation"
              result="blur"
            ></feGaussianBlur>
            <feColorMatrix
              in="blur"
              type="matrix"
              values="
              1 0 0 0 0  
              0 1 0 0 0  
              0 0 1 0 0  
              0 0 0 36 -12"
              result="goo"
            ></feColorMatrix>
            <feComposite
              in="SourceGraphic"
              in2="goo"
              operator="atop"
            ></feComposite>
          </filter>
        </defs>
      </svg>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";

interface Props {
  tabs: string[];
  activeTab: string;
  margin?: number;
  class?: string;
  blurStdDeviation?: number;
}

const props = withDefaults(defineProps<Props>(), {
  margin: 20,
  blurStdDeviation: 6,
});
const emit = defineEmits<{
  (e: "update:activeTab", tab: string): void;
}>();
</script>

<style></style>

```

## API

| Prop Name          | Type       | Default | Description                                    |
| ------------------ | ---------- | ------- | ---------------------------------------------- |
| `class`            | `string`   | `""`    | Additional class names to style the component. |
| `tabs`             | `string[]` | `[]`    | Tabs.                                          |
| `activeTab`        | `string`   | `""`    | Current active Tab.                            |
| `margin`           | `number`   | `20`    | Active tab margin left and right.              |
| `blurStdDeviation` | `number`   | `6`     | Svg blur stdDeviation, tab rounded use it.     |

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Inspired and ported from [@Preet "Exclusion tabs"](https://x.com/wickedmishra/status/1823026659894940124).

URL: https://inspira-ui.com/components/miscellaneous/multi-step-loader

---
title: Multi Step Loader
description: A step loader for screens that works with async conditions too.
---

```vue
<template>
  <div class="flex flex-col items-start gap-4">
    <!-- Simple Loading Demo -->
    <section class="flex w-full flex-col items-center justify-center">
      <h2 class="mb-2 text-lg font-semibold">Simple Loading Demo (prevent close)</h2>
      <MultiStepLoader
        :steps="simpleLoadingSteps"
        :loading="uiState.isSimpleLoading"
        :prevent-close="true"
        @state-change="handleStateChange"
        @complete="handleComplete"
      />
      <button
        class="mt-4 rounded-md bg-black px-4 py-2 text-sm font-medium text-white dark:bg-white dark:text-black"
        @click="toggleSimpleLoading"
      >
        {{ uiState.isSimpleLoading ? "Stop Loading" : "Start Simple Loading" }}
      </button>
    </section>
    <hr class="my-4 h-px w-full bg-gray-200" />
    <!-- Async Loading Demo -->
    <section class="flex w-full flex-col items-center justify-center">
      <h2 class="mb-2 text-lg font-semibold">Async Loading Demo</h2>
      <MultiStepLoader
        :steps="asyncLoadingSteps"
        :loading="uiState.isAfterTextLoading"
        @state-change="handleStateChange"
        @complete="handleComplete"
        @close="uiState.closeAsync"
      />
      <button
        class="mt-4 rounded-md bg-black px-4 py-2 text-sm font-medium text-white dark:bg-white dark:text-black"
        :disabled="uiState.isAfterTextLoading"
        @click="startAsyncLoading"
      >
        Start Async Loading
      </button>
    </section>
  </div>
</template>

<script setup lang="ts">
import { reactive, computed } from "vue";

interface Step {
  text: string; // Display text for the step
  afterText?: string; // Text to show after step completion
  async?: boolean; // If true, waits for external trigger to proceed
  duration?: number; // Duration in ms before proceeding (default: 2000)
  action?: () => void; // Function to execute when step is active
}
// State management
const loaderStates = reactive({
  isProcessing: false,
  isSavingOrder: false,
  sendingMails: false,
});

const uiState = reactive({
  isSimpleLoading: false,
  isAfterTextLoading: false,
  closeSimple: () => {
    uiState.isSimpleLoading = false;
  },
  closeAsync: () => {
    uiState.isAfterTextLoading = false;
  },
});

// Simple loading steps configuration
const simpleLoadingSteps = computed<Step[]>(() => [
  {
    text: "Checking Payment",
    duration: 2000,
  },
  {
    text: "Saving Order",
    duration: 1500,
  },
  {
    text: "Sending Confirmation Email",
    duration: 2500,
  },
  {
    text: "Processing Request",
    duration: 1800,
  },
  {
    text: "Finalizing",
    duration: 1000,
  },
  {
    text: "Redirecting",
    duration: 1000,
    action: handleSimpleLoadingComplete,
  },
]);

// Async loading steps configuration
const asyncLoadingSteps = computed<Step[]>(() => [
  {
    text: "Checking Payment",
    async: loaderStates.isProcessing,
    afterText: "Payment Verified",
  },
  {
    text: "Saving Order",
    async: loaderStates.isSavingOrder,
    afterText: "Order Saved",
  },
  {
    text: "Sending Confirmation Email",
    async: loaderStates.sendingMails,
    afterText: "Email Sent",
  },
  {
    text: "Redirecting",
    duration: 1000,
    action: handleAsyncLoadingComplete,
  },
]);

// Event handlers
function handleStateChange(state: number) {
  // Handle Loading State Change
}

function handleComplete() {
  // Handle Loading Complete
}

function handleSimpleLoadingComplete() {
  alert("Simple loading complete, redirecting...");
  uiState.isSimpleLoading = false;
}

function handleAsyncLoadingComplete() {
  alert("Async loading complete, redirecting...");
  uiState.isAfterTextLoading = false;
}

// Action handlers
function toggleSimpleLoading() {
  uiState.isSimpleLoading = !uiState.isSimpleLoading;
}

async function startAsyncLoading() {
  // Reset states
  uiState.isAfterTextLoading = true;
  loaderStates.isProcessing = true;
  loaderStates.isSavingOrder = true;
  loaderStates.sendingMails = true;

  // Simulate async operations
  function simulateAsyncStep(stateProp: keyof typeof loaderStates, delay: number) {
    return new Promise<void>((resolve) => {
      setTimeout(() => {
        loaderStates[stateProp] = false;
        resolve();
      }, delay);
    });
  }

  try {
    await simulateAsyncStep("isProcessing", 2000);
    await simulateAsyncStep("isSavingOrder", 3000);
    await simulateAsyncStep("sendingMails", 2500);
  } catch (error) {
    uiState.isAfterTextLoading = false;
  }
}
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="multi-step-loader" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <Transition
    enter-active-class="transition-opacity duration-300"
    enter-from-class="opacity-0"
    enter-to-class="opacity-100"
    leave-active-class="transition-opacity duration-300"
    leave-from-class="opacity-100"
    leave-to-class="opacity-0"
  >
    <div
      v-if="loading && steps.length > 0"
      class="fixed inset-0 z-[100] flex size-full items-center justify-center backdrop-blur-2xl"
    >
      <!-- Closing Button -->
      <button
        v-show="!preventClose"
        class="absolute right-4 top-4 z-[101] inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
        size="sm"
        @click="close"
      >
        <!-- x-mark-heroicons -->
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          stroke-width="1.5"
          stroke="currentColor"
          class="size-6"
        >
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            d="M6 18 18 6M6 6l12 12"
          />
        </svg>
      </button>
      <div class="relative h-96">
        <div class="relative mx-auto mt-40 flex max-w-xl flex-col justify-start">
          <div
            v-for="(step, index) in steps"
            :key="index"
          >
            <div
              v-if="step"
              class="mb-4 flex items-center gap-2 text-left transition-all duration-300 ease-in-out"
              :style="{
                opacity:
                  index === currentState
                    ? 1
                    : Math.max(1 - Math.abs(index - currentState) * 0.2, 0),
                transform: `translateY(${
                  index === currentState ? -(currentState * 40) : -(currentState * 40)
                }px)`,
              }"
            >
              <!--  check-circle-solid-heroicons -->
              <svg
                v-if="
                  index < currentState ||
                  (index === steps.length - 1 && index === currentState && isLastStepComplete)
                "
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="currentColor"
                class="size-6 text-primary"
              >
                <path
                  fill-rule="evenodd"
                  d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
                  clip-rule="evenodd"
                />
              </svg>
              <!-- arrow-path-heroicons -->
              <svg
                v-else-if="
                  index === currentState && (!isLastStepComplete || index !== steps.length - 1)
                "
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="currentColor"
                class="size-6 animate-spin text-primary"
              >
                <path
                  fill-rule="evenodd"
                  d="M4.755 10.059a7.5 7.5 0 0 1 12.548-3.364l1.903 1.903h-3.183a.75.75 0 1 0 0 1.5h4.992a.75.75 0 0 0 .75-.75V4.356a.75.75 0 0 0-1.5 0v3.18l-1.9-1.9A9 9 0 0 0 3.306 9.67a.75.75 0 1 0 1.45.388Zm15.408 3.352a.75.75 0 0 0-.919.53 7.5 7.5 0 0 1-12.548 3.364l-1.902-1.903h3.183a.75.75 0 0 0 0-1.5H2.984a.75.75 0 0 0-.75.75v4.992a.75.75 0 0 0 1.5 0v-3.18l1.9 1.9a9 9 0 0 0 15.059-4.035.75.75 0 0 0-.53-.918Z"
                  clip-rule="evenodd"
                />
              </svg>
              <!-- check-circle-outline-heroicons -->
              <svg
                v-else
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke-width="1.5"
                stroke="currentColor"
                class="size-6 text-black opacity-50 dark:text-white"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
                />
              </svg>
              <div class="flex flex-col">
                <span
                  :class="[
                    'text-lg text-black dark:text-white',
                    index > currentState && 'opacity-50',
                  ]"
                >
                  {{ step.text }}
                </span>
                <Transition
                  enter-active-class="transition-all duration-300"
                  enter-from-class="opacity-0 -translate-y-1"
                  enter-to-class="opacity-100 translate-y-0"
                >
                  <span
                    v-if="
                      step.afterText &&
                      (index < currentState ||
                        (index === steps.length - 1 &&
                          index === currentState &&
                          isLastStepComplete))
                    "
                    class="mt-1 text-sm text-gray-500 dark:text-gray-400"
                  >
                    {{ step.afterText }}
                  </span>
                </Transition>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div
        class="absolute inset-x-0 bottom-0 z-20 h-full bg-white bg-gradient-to-t [mask-image:radial-gradient(900px_at_center,transparent_30%,white)] dark:bg-black"
      ></div>
    </div>
  </Transition>
</template>

<script setup lang="ts">
import { ref, watch, onUnmounted } from "vue";

interface Step {
  text: string; // Display text for the step
  afterText?: string; // Text to show after step completion
  async?: boolean; // If true, waits for external trigger to proceed
  duration?: number; // Duration in ms before proceeding (default: 2000)
  action?: () => void; // Function to execute when step is active
}
interface Props {
  steps: Step[];
  loading?: boolean;
  defaultDuration?: number;
  preventClose?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  loading: false,
  defaultDuration: 1500,
  preventClose: false,
});

const emit = defineEmits<{
  "state-change": [number];
  complete: [];
  close: [];
}>();

const currentState = ref(0);
const stepStartTime = ref(Date.now());
const isLastStepComplete = ref(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let currentTimer: any = null;

async function executeStepAction(step: Step) {
  if (typeof step.action === "function") {
    await step.action();
  }
}

async function proceedToNextStep() {
  const currentStep = props.steps[currentState.value];
  if (!currentStep) return;

  // Execute the current step's action
  await executeStepAction(currentStep);

  if (currentState.value < props.steps.length - 1) {
    currentState.value++;
    stepStartTime.value = Date.now();
    emit("state-change", currentState.value);
    processCurrentStep();
  } else {
    isLastStepComplete.value = true;
    emit("complete");
  }
}

async function processCurrentStep() {
  if (currentTimer) {
    clearTimeout(currentTimer);
  }

  const currentStep = props.steps[currentState.value];
  if (!currentStep) return;

  const duration = currentStep.duration || props.defaultDuration;

  if (!currentStep.async) {
    currentTimer = setTimeout(() => {
      proceedToNextStep();
    }, duration);
  }
}

function close() {
  emit("close");
}

// Watch for changes in the async property
watch(
  () => props.steps[currentState.value]?.async,
  async (isAsync, oldIsAsync) => {
    // Only proceed if changing from async to non-async
    if (isAsync === false && oldIsAsync === true) {
      const currentStep = props.steps[currentState.value];
      if (!currentStep) return;

      const duration = currentStep.duration || props.defaultDuration;
      currentTimer = setTimeout(() => {
        proceedToNextStep();
      }, duration);
    }
  },
);

watch(
  () => props.loading,
  (newLoading) => {
    if (newLoading) {
      currentState.value = 0;
      stepStartTime.value = Date.now();
      isLastStepComplete.value = false;
      processCurrentStep();
    } else if (currentTimer) {
      clearTimeout(currentTimer);
    }
  },
);

onUnmounted(() => {
  if (currentTimer) {
    clearTimeout(currentTimer);
  }
});
</script>

```

## API

| Prop Name         | Type      | Default | Description                                                                  |
| ----------------- | --------- | ------- | ---------------------------------------------------------------------------- |
| `loading`         | `boolean` | `false` | Controls the visibility of the loader. When `true`, the loader is displayed. |
| `steps`           | `Step[]`  | `[]`    | Array of step objects defining the loading sequence.                         |
| `defaultDuration` | `number`  | `1500`  | The duration of each step in milliseconds.                                   |
| `preventClose`    | `boolean` | `false` | If `true`, the close button will not be shown.                               |

| Event Name     | Payload Type | Description                                                          |
| -------------- | ------------ | -------------------------------------------------------------------- |
| `state-change` | `number`     | Emitted when the current step changes, providing the new step index. |
| `complete`     | `void`       | Emitted when all steps have been completed.                          |
| `close`        | `void`       | Emitted when the loader is closed by button.                         |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for this component.

- Inspired from [Aceternity UI's Multi Step Loader](https://ui.aceternity.com/components/multi-step-loader).

URL: https://inspira-ui.com/components/miscellaneous/photo-gallery

---
title: Photo Gallery
description: Showcase your team with pride using our stunning Photo Gallery Component.
---

```vue
<template>
  <div class="flex items-center justify-center">
    <PhotoGallery :items="items" />
  </div>
</template>

<script lang="ts" setup>
const items = [
  {
    src: "https://images.pexels.com/photos/16834200/pexels-photo-16834200/free-photo-of-young-brunette-in-a-white-dress-sitting-on-a-bench-and-holding-flowers.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
  {
    src: "https://images.pexels.com/photos/16834202/pexels-photo-16834202/free-photo-of-young-woman-in-a-white-dress-posing-outdoors.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
  {
    src: "https://images.pexels.com/photos/16834194/pexels-photo-16834194/free-photo-of-woman-in-white-dress-posing-with-red-flowers-on-lap.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
  {
    src: "https://images.pexels.com/photos/17362900/pexels-photo-17362900/free-photo-of-pretty-young-model-in-chinese-retro-dress.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
  {
    src: "https://images.pexels.com/photos/19447919/pexels-photo-19447919/free-photo-of-model-in-a-pink-ao-dai-dress-with-a-bouquet-of-tulips-by-the-river.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
  {
    src: "https://images.pexels.com/photos/20332975/pexels-photo-20332975/free-photo-of-young-woman-posing-in-a-silk-slip-dress.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
  {
    src: "https://images.pexels.com/photos/19732643/pexels-photo-19732643/free-photo-of-woman-in-white-dress-sitting-and-reading-book.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
  {
    src: "https://images.pexels.com/photos/17347482/pexels-photo-17347482/free-photo-of-woman-in-dress-posing-with-bag.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load",
  },
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="photo-gallery" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    class="gallery"
    :class="cn('mb-[var(--size)] grid grid-cols-6 gap-1', props.containerClass)"
  >
    <img
      v-for="(image, index) in props.items"
      :key="index"
      :src="image.src"
      :alt="`image+${index}`"
      class="gallery-img"
      :class="
        cn(
          'size-[calc(var(--size)*2)] rounded object-cover transition-[clip-path,filter] duration-75',
          props.class,
        )
      "
    />
  </div>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";

interface Props {
  containerClass?: string;
  class?: string;
  items: {
    src: string;
  }[];
}
const props = defineProps<Props>();
</script>

<style scoped>
.gallery {
  --size: 100px;
  grid-auto-rows: var(--size);

  &:has(:hover) img:not(:hover),
  &:has(:focus) img:not(:focus) {
    filter: brightness(0.5) contrast(0.5);
  }

  img {
    clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
    grid-column: auto / span 2;

    &:nth-child(5n-1) {
      grid-column: 2 / span 2;
    }

    &:hover,
    &:focus {
      clip-path: polygon(100% 0, 100% 100%, 0 100%, 0 0);
      z-index: 1;
      transition:
        clip-path 0.25s,
        filter 0.25s;
      filter: saturate(150%);
    }

    &:focus {
      outline: 10px dashed black;
      outline-offset: -5px;
    }
  }
}
</style>

```

## API

| Prop Name        | Type                | Default | Description                                            |
| ---------------- | ------------------- | ------- | ------------------------------------------------------ |
| `items`          | `"[{src: string}]"` | `[]`    | Pass items / image src to animate                      |
| `containerClass` | `string`            | `""`    | Additional tailwind CSS classes for container styling. |
| `class`          | `string`            | `""`    | Additional tailwind CSS classes for custom styling.    |

## Credits

- All images from [Pexels](https://www.pexels.com/@soldiervip/)
- Thanks to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for providing this component.

URL: https://inspira-ui.com/components/miscellaneous/scroll-island

---
title: Scroll Island
description: A dynamic and interactive component that displays scroll progress with animated visuals and an expandable area for additional content.
---

```vue
<template>
  <div class="flex items-center justify-center p-8">
    <span class="text-lg text-primary">
      See the scroll island in action on the top of this page
    </span>
    <ScrollIsland title="Scroll Island">
      <div class="my-3 flex flex-col gap-2">
        <a href="#api"># API</a>
        <a href="#component-code"># Component Code</a>
        <a href="#features"># Features</a>
        <a href="#credits"># Credits</a>
      </div>
    </ScrollIsland>
  </div>
</template>

```

::alert{type="info"}
**Note:** This component requires `@number-flow/vue` as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="scroll-island" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install @number-flow/vue
```

```bash [pnpm]
pnpm install @number-flow/vue
```

```bash [bun]
bun add @number-flow/vue
```

```bash [yarn]
yarn add @number-flow/vue
```

::

Copy and paste the following code

```vue
<template>
  <MotionConfig
    :transition="{
      duration: 0.7,
      type: 'spring',
      bounce: 0.5,
    }"
  >
    <div
      :class="
        cn(
          'fixed left-1/2 top-12 z-[999] -translate-x-1/2 bg-primary/90 backdrop-blur-lg border-radius',
          $props.class,
        )
      "
      @click="() => (open = !open)"
    >
      <motion.div
        id="motion-id"
        layout
        :initial="{
          height: props.height,
          width: 0,
        }"
        :animate="{
          height: open && isSlotAvailable ? 'auto' : props.height,
          width: open && isSlotAvailable ? 320 : 260,
        }"
        class="bg-natural-900 relative cursor-pointer overflow-hidden text-secondary"
      >
        <header class="gray- flex h-11 cursor-pointer items-center gap-2 px-4">
          <AnimatedCircularProgressBar
            :value="scrollPercentage * 100"
            :min="0"
            :max="100"
            :circle-stroke-width="10"
            class="w-6"
            :show-percentage="false"
            :duration="0.3"
            :gauge-secondary-color="isDark ? '#6b728055' : '#6b728099'"
            :gauge-primary-color="isDark ? 'black' : 'white'"
          />
          <h1 class="grow text-center font-bold">{{ title }}</h1>
          <NumberFlow
            :value="scrollPercentage"
            :format="{ style: 'percent' }"
            locales="en-US"
          />
        </header>
        <motion.div
          v-if="isSlotAvailable"
          class="mb-2 flex h-full max-h-60 flex-col gap-1 overflow-y-auto px-4 text-sm"
        >
          <slot />
        </motion.div>
      </motion.div>
    </div>
  </MotionConfig>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import NumberFlow from "@number-flow/vue";
import { useColorMode } from "@vueuse/core";
import { motion, MotionConfig } from "motion-v";
import { computed, onMounted, onUnmounted, ref, useSlots } from "vue";

interface Props {
  class?: string;
  title?: string;
  height?: number;
}

const props = withDefaults(defineProps<Props>(), {
  class: "",
  title: "Progress",
  height: 44,
});

const open = ref(false);
const slots = useSlots();

const scrollPercentage = ref(0);

const isDark = computed(() => useColorMode().value == "dark");
const isSlotAvailable = computed(() => !!slots.default);
const borderRadius = computed(() => `${props.height / 2}px`);

onMounted(() => {
  if (window === undefined) return;

  window.addEventListener("scroll", updatePageScroll);
  updatePageScroll();
});

function updatePageScroll() {
  scrollPercentage.value = window.scrollY / (document.body.scrollHeight - window.innerHeight);
}

onUnmounted(() => {
  window.removeEventListener("scroll", updatePageScroll);
});
</script>

<style scoped>
.border-radius {
  border-radius: v-bind(borderRadius);
}
</style>

```
::

## API

| Prop Name | Type     | Default      | Description                                     |
| --------- | -------- | ------------ | ----------------------------------------------- |
| `class`   | `string` | `""`         | Additional CSS classes for custom styling.      |
| `title`   | `string` | `"Progress"` | Title displayed in the header of the component. |
| `height`  | `string` | `44`         | Height of the component.                        |

## Features

- **Scroll Progress Tracking**: Dynamically displays the scroll progress of the page as a percentage.
- **Expandable Layout**: Transforms between a circular and a rectangular layout based on user interaction.
- **Animated Circular Progress Bar**: Displays a visually appealing progress bar with smooth transitions.
- **Dynamic Content Slot**: Supports additional content in the expandable section.
- **Dark Mode Support**: Adapts to the dark or light mode of the user's system preferences.

## Credits

- Inspired by the work of [Ali Samadi](https://x.com/alisamadi__/status/1854312982559502556) & [Nitish Khagwal](https://x.com/nitishkmrk)
- [NumberFlow by Maxwell Barvian](https://number-flow.barvian.me/vue) for number formatting and animations.

URL: https://inspira-ui.com/components/miscellaneous/svg-mask

---
title: SVG Mask
description: A dynamic SVG mask component that reveals content with hover and mouse movement.
---

```vue
<template>
  <div class="flex h-[40rem] w-full items-center justify-center overflow-hidden">
    <SVGMask class="h-[40rem] rounded-md border">
      <template #base>
        The first rule of <span class="text-red-500">MRR Club</span> is you do not talk about MRR
        Club. The second rule of MRR Club is you DO NOT talk about
        <span class="text-red-500">MRR Club</span>.
      </template>
      <template #reveal>
        <p class="mx-auto max-w-4xl text-center text-4xl font-bold text-slate-800">
          The first rule of MRR Club is you do not talk about MRR Club. The second rule of MRR Club
          is you DO NOT talk about MRR Club.
        </p>
      </template>
    </SVGMask>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="svg-mask" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="SVGMask.vue" language="vue" componentName="SVGMask" type="ui" id="svg-mask"}

```html [mask.svg]
<svg
  width="1298"
  height="1298"
  viewBox="0 0 1298 1298"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <circle
    cx="649"
    cy="649"
    r="649"
    fill="black"
  />
</svg>
```

::

## API

| Prop Name    | Type     | Default | Description                                |
| ------------ | -------- | ------- | ------------------------------------------ |
| `class`      | `string` | `""`    | Additional CSS classes for custom styling. |
| `size`       | `number` | `10`    | Initial size of the mask in pixels.        |
| `revealSize` | `number` | `600`   | Size of the mask during hover in pixels.   |

## Features

- **Hover-Activated Mask**: The SVG mask dynamically enlarges when hovered, revealing the content beneath.
- **Mouse Tracking**: The mask follows the cursor, creating an engaging and interactive effect.
- **Customizable Mask Size**: Adjust the initial size (`size`) and hover size (`revealSize`) for different effects.
- **Slot-Based Content**: Supports named slots for base and reveal content, making it flexible for various use cases.
- **Responsive Background**: Includes hover-based background color transitions for added visual appeal.

## Credits

- Ported from [Aceternity UI's SVG Mask Effect](https://ui.aceternity.com/components/text-generate-effect).

URL: https://inspira-ui.com/components/miscellaneous/timeline

---
title: Timeline
description: A visually appealing and interactive timeline component with smooth animations, sticky labels, and a gradient scrolling effect.
---

```vue
<template>
  <div class="h-fit w-full">
    <Timeline
      :items="data"
      title="Beam me up"
      description="Show the timeline in style"
    >
      <template
        v-for="(item, index) in data"
        :key="item.id + 'template'"
        #[item.id]
      >
        <div class="relative w-full pl-20 pr-4 md:pl-4">
          <h3
            class="mb-4 block text-left text-2xl font-bold text-neutral-500 dark:text-neutral-500"
          >
            {{ `Section ${index + 1}` }}
          </h3>
        </div>
        <p className="text-neutral-800 dark:text-neutral-200 text-xs md:text-sm font-normal mb-8">
          Inspira UI gives you the freedom to design awesome website, in less time.
        </p>
      </template>
    </Timeline>
  </div>
</template>

<script setup lang="ts">
const data = [
  {
    id: "version1.0",
    label: "Version 1.0",
  },
  {
    id: "version2.0",
    label: "Version 2.0",
  },

  {
    id: "version3.0",
    label: "Version 3.0",
  },
  {
    id: "version4.0",
    label: "Version 4.0",
  },

  {
    id: "version5.0",
    label: "Version 5.0",
  },
  {
    id: "version6.0",
    label: "Version 6.0",
  },
];
</script>

```

## API

| Prop Name        | Type                               | Default | Description                                        |
| ---------------- | ---------------------------------- | ------- | -------------------------------------------------- |
| `containerClass` | `string`                           | `""`    | Additional CSS classes for the timeline container. |
| `class`          | `string`                           | `""`    | Additional CSS classes for styling.                |
| `items`          | `{ id: string; label: string; }[]` | `[]`    | List of timeline items, each with an ID and label. |
| `title`          | `string`                           | `""`    | Title of the timeline section.                     |
| `description`    | `string`                           | `""`    | Description text displayed below the title.        |

## Install using CLI

```vue
<InstallationCli component-id="timeline" />
```

## Install Manually

You can copy and paste the following code to create this component:

::code-group

    ::CodeViewerTab{label="Timeline.vue" language="vue" componentName="Timeline" type="ui" id="timeline"}
    ::

::

## Features

- **Animated Scroll Effect**: The timeline bar animates smoothly as the user scrolls.
- **Sticky Labels**: Each event label remains visible while scrolling.
- **Gradient Progress Bar**: A visually appealing timeline indicator with gradient effects.
- **Slot Support**: Custom content can be placed within each timeline item.
- **Responsive Design**: Adapts seamlessly to different screen sizes.
- **Dark Mode Support**: Automatically adjusts to light or dark themes.

## Credits

- Inspired and ported from [Aceternity UI's Timeline](https://ui.aceternity.com/components/timeline).

URL: https://inspira-ui.com/components/miscellaneous/tracing-beam

---
title: Tracing Beam
description: A component that renders a tracing beam effect with dynamic scrolling animations and gradient strokes.
---

```vue
<template>
  <div class="w-full items-center justify-center px-8">
    <TracingBeam class="px-6">
      <div class="relative mx-auto max-w-2xl pt-4 antialiased">
        <div
          v-for="(item, index) in dummyContent"
          :key="`content-${index}`"
          class="mb-10"
        >
          <div
            class="mb-4 w-fit rounded-full bg-black px-2 text-sm text-white dark:bg-white dark:text-black"
          >
            {{ item.badge }}
          </div>

          <p :class="['mb-4 text-xl']">
            {{ item.title }}
          </p>

          <div class="prose prose-sm dark:prose-invert text-sm">
            <img
              v-if="item.image"
              :src="item.image"
              alt="blog thumbnail"
              class="mb-10 rounded-lg object-cover"
            />
            <div>
              <p
                v-for="(paragraph, idx) in item.description"
                :key="`desc-${idx}`"
              >
                {{ paragraph }}
              </p>
            </div>
          </div>
        </div>
      </div>
    </TracingBeam>
  </div>
</template>

<script setup lang="ts">
// Dummy content data
const dummyContent = [
  {
    title: "Lorem Ipsum Dolor Sit Amet",
    description: [
      "Sit duis est minim proident non nisi velit non consectetur. Esse adipisicing laboris consectetur enim ipsum reprehenderit eu deserunt Lorem ut aliqua anim do. Duis cupidatat qui irure cupidatat incididunt incididunt enim magna id est qui sunt fugiat. Laboris do duis pariatur fugiat Lorem aute sit ullamco. Qui deserunt non reprehenderit dolore nisi velit exercitation Lorem qui do enim culpa. Aliqua eiusmod in occaecat reprehenderit laborum nostrud fugiat voluptate do Lorem culpa officia sint labore. Tempor consectetur excepteur ut fugiat veniam commodo et labore dolore commodo pariatur.",
      "Dolor minim irure ut Lorem proident. Ipsum do pariatur est ad ad veniam in commodo id reprehenderit adipisicing. Proident duis exercitation ad quis ex cupidatat cupidatat occaecat adipisicing.",
      "Tempor quis dolor veniam quis dolor. Sit reprehenderit eiusmod reprehenderit deserunt amet laborum consequat adipisicing officia qui irure id sint adipisicing. Adipisicing fugiat aliqua nulla nostrud. Amet culpa officia aliquip deserunt veniam deserunt officia adipisicing aliquip proident officia sunt.",
    ],
    badge: "Vue",
    image:
      "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&q=80&w=3540&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    title: "Lorem Ipsum Dolor Sit Amet",
    description: [
      "Ex irure dolore veniam ex velit non aute nisi labore ipsum occaecat deserunt cupidatat aute. Enim cillum dolor et nulla sunt exercitation non voluptate qui aliquip esse tempor. Ullamco ut sunt consectetur sint qui qui do do qui do. Labore laborum culpa magna reprehenderit ea velit id esse adipisicing deserunt amet dolore. Ipsum occaecat veniam commodo proident aliqua id ad deserunt dolor aliquip duis veniam sunt.",
      "In dolore veniam excepteur eu est et sunt velit. Ipsum sint esse veniam fugiat esse qui sint ad sunt reprehenderit do qui proident reprehenderit. Laborum exercitation aliqua reprehenderit ea sint cillum ut mollit.",
    ],
    badge: "Nuxt",
    image:
      "https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&q=80&w=3540&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
  {
    title: "Lorem Ipsum Dolor Sit Amet",
    description: [
      "Ex irure dolore veniam ex velit non aute nisi labore ipsum occaecat deserunt cupidatat aute. Enim cillum dolor et nulla sunt exercitation non voluptate qui aliquip esse tempor. Ullamco ut sunt consectetur sint qui qui do do qui do. Labore laborum culpa magna reprehenderit ea velit id esse adipisicing deserunt amet dolore. Ipsum occaecat veniam commodo proident aliqua id ad deserunt dolor aliquip duis veniam sunt.",
    ],
    badge: "Inspira UI",
    image:
      "https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&q=80&w=3506&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  },
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="tracing-beam" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="tracingBeamRef"
    :class="cn('relative w-full max-w-4xl mx-auto h-full', $props.class)"
  >
    <div class="absolute -left-4 top-3 md:-left-12">
      <div
        :style="{
          boxShadow: scrollYProgress > 0 ? 'none' : 'rgba(0, 0, 0, 0.24) 0px 3px 8px',
        }"
        class="border-netural-200 ml-[27px] flex size-4 items-center justify-center rounded-full border shadow-sm"
      >
        <Motion
          :animate="{
            backgroundColor: scrollYProgress > 0 ? 'white' : 'var(--emerald-500)',
            borderColor: scrollYProgress > 0 ? 'white' : 'var(--emerald-600)',
          }"
          class="size-2 rounded-full border border-neutral-300 bg-white"
        />
      </div>
      <svg
        :viewBox="`0 0 20 ${svgHeight}`"
        width="20"
        :height="svgHeight"
        class="ml-4 block"
        aria-hidden="true"
      >
        <path
          :d="`M 1 0V -36 l 18 24 V ${svgHeight * 0.8} l -18 24V ${svgHeight}`"
          fill="none"
          stroke="#9091A0"
          stroke-opacity="0.16"
        ></path>
        <path
          :d="`M 1 0V -36 l 18 24 V ${svgHeight * 0.8} l -18 24V ${svgHeight}`"
          fill="none"
          stroke="url(#gradient)"
          stroke-width="1.25"
          class="motion-reduce:hidden"
        ></path>
        <defs>
          <linearGradient
            id="gradient"
            gradientUnits="userSpaceOnUse"
            x1="0"
            x2="0"
            :y1="spring.y1"
            :y2="spring.y2"
          >
            <stop
              stop-color="#18CCFC"
              stop-opacity="0"
            ></stop>
            <stop stop-color="#18CCFC"></stop>
            <stop
              offset="0.325"
              stop-color="#6344F5"
            ></stop>
            <stop
              offset="1"
              stop-color="#AE48FF"
              stop-opacity="0"
            ></stop>
          </linearGradient>
        </defs>
      </svg>
    </div>
    <div ref="tracingBeamContentRef">
      <slot />
    </div>
  </div>
</template>

<script lang="ts" setup>
import { Motion } from "motion-v";
import { cn } from "@/lib/utils";
import { useSpring } from "vue-use-spring";
import { ref, computed, onMounted, onUnmounted, watch } from "vue";

defineProps({
  class: String,
});

const tracingBeamRef = ref<HTMLDivElement>();
const tracingBeamContentRef = ref<HTMLDivElement>();

const scrollYProgress = ref(0);
const svgHeight = ref(0);
const scrollPercentage = ref(0);

const computedY1 = computed(
  () =>
    mapRange(scrollYProgress.value, 0, 0.8, scrollYProgress.value, svgHeight.value) *
    (1.4 - scrollPercentage.value),
);

const computedY2 = computed(
  () =>
    mapRange(scrollYProgress.value, 0, 1, scrollYProgress.value, svgHeight.value - 500) *
    (1.4 - scrollPercentage.value),
);

const spring = useSpring(
  { y1: computedY1.value, y2: computedY2.value },
  { tension: 80, friction: 26, precision: 0.01 },
);

watch(computedY1, (newY1) => {
  spring.y1 = newY1;
});

watch(computedY2, (newY2) => {
  spring.y2 = newY2;
});

function updateScrollYProgress() {
  if (tracingBeamRef.value) {
    const boundingRect = tracingBeamRef.value.getBoundingClientRect();
    const windowHeight = window.innerHeight;
    const elementHeight = boundingRect.height;

    scrollPercentage.value = (windowHeight - boundingRect.top) / (windowHeight + elementHeight);

    scrollYProgress.value = (boundingRect.y / windowHeight) * -1;
  }
}

onMounted(() => {
  window.addEventListener("scroll", updateScrollYProgress);
  window.addEventListener("resize", updateScrollYProgress);
  updateScrollYProgress();

  const resizeObserver = new ResizeObserver(function () {
    updateSVGHeight();
  });

  resizeObserver.observe(tracingBeamContentRef.value!);

  updateSVGHeight();
});

onUnmounted(() => {
  tracingBeamRef.value?.removeEventListener("scroll", updateScrollYProgress);
  window.removeEventListener("resize", updateScrollYProgress);
});

function updateSVGHeight() {
  if (!tracingBeamContentRef.value) return;

  svgHeight.value = tracingBeamContentRef.value.offsetHeight;
}

function mapRange(
  value: number,
  inMin: number,
  inMax: number,
  outMin: number,
  outMax: number,
): number {
  return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
}
</script>

```

## API

| Prop Name | Type     | Default | Description                                |
| --------- | -------- | ------- | ------------------------------------------ |
| `class`   | `string` | `""`    | Additional CSS classes for custom styling. |

## Features

- **Dynamic Tracing Beam**: Renders a gradient tracing beam that follows the scroll position, adding a modern visual effect.
- **Scroll-Based Animation**: As the user scrolls, the beam animates along a path with varying gradients, responding to scroll depth.
- **Gradient Transition**: Smoothly transitions colors along the beam from cyan to purple with fading edges for a subtle effect.
- **Slot-Based Content**: Supports a default slot to add content inside the tracing beam container.

## Credits

- Ported from [Aceternity UI](https://ui.aceternity.com/components/tracing-beam);

URL: https://inspira-ui.com/components/special-effects/animated-beam

---
title: Animated Beam
description: An SVG beam connecting elements with animation.
---

```vue
<template>
  <div
    ref="containerRef"
    class="relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-lg border bg-background p-10 md:shadow-xl"
  >
    <div
      class="flex size-full max-h-[200px] max-w-lg flex-col items-stretch justify-between gap-10"
    >
      <div class="flex flex-row items-center justify-between">
        <div
          ref="div1Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)] dark:text-black"
        >
          <Icon
            name="devicon:googlecloud"
            size="24"
          />
        </div>
        <div
          ref="div5Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
        >
          <Icon
            name="logos:google-drive"
            size="24"
          />
        </div>
      </div>
      <div class="flex flex-row items-center justify-between">
        <div
          ref="div2Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)] dark:text-black"
        >
          <Icon
            name="logos:notion-icon"
            size="24"
          />
        </div>
        <div
          ref="div4Ref"
          class="z-10 flex size-16 items-center justify-center rounded-full border-2 bg-white p-2 text-black shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
        >
          <Icon
            name="simple-icons:openai"
            size="30"
          />
        </div>
        <div
          ref="div6Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
        >
          <Icon
            name="logos:google-gmail"
            size="24"
          />
        </div>
      </div>
      <div class="flex flex-row items-center justify-between">
        <div
          ref="div3Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)] dark:text-black"
        >
          <Icon
            name="logos:whatsapp-icon"
            size="24"
          />
        </div>
        <div
          ref="div7Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
        >
          <Icon
            name="logos:messenger"
            size="24"
          />
        </div>
      </div>
    </div>

    <AnimatedBeam
      :container-ref="containerRef"
      :from-ref="div1Ref"
      :to-ref="div4Ref"
      :curvature="-75"
      :end-y-offset="-10"
    />
    <AnimatedBeam
      :container-ref="containerRef"
      :from-ref="div2Ref"
      :to-ref="div4Ref"
    />
    <AnimatedBeam
      :container-ref="containerRef"
      :from-ref="div3Ref"
      :to-ref="div4Ref"
      :curvature="75"
      :end-y-offset="10"
    />
    <AnimatedBeam
      :container-ref="containerRef"
      :from-ref="div5Ref"
      :to-ref="div4Ref"
      :curvature="-75"
      :end-y-offset="-10"
    />
    <AnimatedBeam
      :container-ref="containerRef"
      :from-ref="div6Ref"
      :to-ref="div4Ref"
    />
    <AnimatedBeam
      :container-ref="containerRef"
      :from-ref="div7Ref"
      :to-ref="div4Ref"
      :curvature="75"
      :end-y-offset="10"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const containerRef = ref(null);
const div1Ref = ref(null);
const div2Ref = ref(null);
const div3Ref = ref(null);
const div4Ref = ref(null);
const div5Ref = ref(null);
const div6Ref = ref(null);
const div7Ref = ref(null);
</script>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="animated-beam" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <svg
    fill="none"
    :width="svgDimensions.width"
    :height="svgDimensions.height"
    xmlns="http://www.w3.org/2000/svg"
    :class="cn('pointer-events-none absolute left-0 top-0 transform-gpu stroke-2', $props.class)"
    :viewBox="`0 0 ${svgDimensions.width} ${svgDimensions.height}`"
  >
    <path
      :d="pathD"
      :stroke="pathColor"
      :stroke-width="pathWidth"
      :stroke-opacity="pathOpacity"
      stroke-linecap="round"
    />
    <path
      :d="pathD"
      :stroke-width="pathWidth"
      :stroke="`url(#${id})`"
      stroke-opacity="1"
      stroke-linecap="round"
    />
    <defs>
      <linearGradient
        :id="id"
        gradientUnits="userSpaceOnUse"
        x1="0%"
        x2="0%"
        y1="0%"
        y2="0%"
      >
        <stop
          :stop-color="gradientStartColor"
          stop-opacity="0"
        />
        <stop :stop-color="gradientStartColor" />
        <stop
          offset="32.5%"
          :stop-color="gradientStopColor"
        />
        <stop
          offset="100%"
          :stop-color="gradientStopColor"
          stop-opacity="0"
        />
        <animate
          v-if="!isVertical"
          attributeName="x1"
          :values="x1"
          :dur="`${duration}s`"
          keyTimes="0; 1"
          keySplines="0.16 1 0.3 1"
          calcMode="spline"
          repeatCount="indefinite"
        />
        <animate
          v-if="!isVertical"
          attributeName="x2"
          :values="x2"
          :dur="`${duration}s`"
          keyTimes="0; 1"
          keySplines="0.16 1 0.3 1"
          calcMode="spline"
          repeatCount="indefinite"
        />
        <animate
          v-if="isVertical"
          attributeName="y1"
          :values="y1"
          :dur="`${duration}s`"
          keyTimes="0; 1"
          keySplines="0.16 1 0.3 1"
          calcMode="spline"
          repeatCount="indefinite"
        />
        <animate
          v-if="isVertical"
          attributeName="y2"
          :values="y2"
          :dur="`${duration}s`"
          keyTimes="0; 1"
          keySplines="0.16 1 0.3 1"
          calcMode="spline"
          repeatCount="indefinite"
        />
      </linearGradient>
    </defs>
  </svg>
</template>

<script lang="ts" setup>
import { onBeforeUnmount, ref, watchEffect } from "vue";
import { cn } from "@/lib/utils";

type AnimatedBeamProps = {
  class?: string;
  containerRef: HTMLElement;
  fromRef: HTMLElement;
  toRef: HTMLElement;
  curvature?: number;
  reverse?: boolean;
  pathColor?: string;
  pathWidth?: number;
  pathOpacity?: number;
  gradientStartColor?: string;
  gradientStopColor?: string;
  delay?: number;
  duration?: number;
  startXOffset?: number;
  startYOffset?: number;
  endXOffset?: number;
  endYOffset?: number;
};

const props = withDefaults(defineProps<AnimatedBeamProps>(), {
  curvature: 0,
  reverse: false,
  duration: Math.random() * 3 + 4,
  delay: 0,
  pathColor: "gray",
  pathWidth: 2,
  pathOpacity: 0.2,
  gradientStartColor: "#FFAA40",
  gradientStopColor: "#9C40FF",
  startXOffset: 0,
  startYOffset: 0,
  endXOffset: 0,
  endYOffset: 0,
});

const id = "beam-" + Math.random().toString(36).substring(2, 10);
const isVertical = ref(false);
const isRightToLeft = ref(false);
const isBottomToTop = ref(false);
const x1 = computed(() => {
  const direction = props.reverse ? !isRightToLeft.value : isRightToLeft.value;
  return direction ? "90%; -10%;" : "10%; 110%;";
});
const x2 = computed(() => {
  const direction = props.reverse ? !isRightToLeft.value : isRightToLeft.value;
  return direction ? "100%; 0%;" : "0%; 100%;";
});
const y1 = computed(() => {
  const direction = props.reverse ? !isBottomToTop.value : isBottomToTop.value;
  return direction ? "90%; -10%;" : "10%; 110%;";
});
const y2 = computed(() => {
  const direction = props.reverse ? !isBottomToTop.value : isBottomToTop.value;
  return direction ? "100%; 0%;" : "0%; 100%;";
});

const pathD = ref("");
const svgDimensions = ref<{ width: number; height: number }>({
  width: 0,
  height: 0,
});

let resizeObserver: ResizeObserver | undefined = undefined;

const { stop: stopEffect } = watchEffect(effect);

function effect() {
  if (resizeObserver == undefined && props.containerRef != null) {
    resizeObserver = new ResizeObserver(() => {
      updatePath();
    });
    resizeObserver.observe(props.containerRef);

    stopEffect();
  }
}

// Function to update the path based on the positions of the elements
function updatePath() {
  if (props.containerRef && props.fromRef && props.toRef) {
    const containerRect = props.containerRef.getBoundingClientRect();
    const rectA = props.fromRef.getBoundingClientRect();
    const rectB = props.toRef.getBoundingClientRect();

    const svgWidth = containerRect.width;
    const svgHeight = containerRect.height;
    svgDimensions.value = { width: svgWidth, height: svgHeight };

    const startX = rectA.left - containerRect.left + rectA.width / 2 + (props.startXOffset ?? 0);
    const startY = rectA.top - containerRect.top + rectA.height / 2 + (props.startYOffset ?? 0);
    const endX = rectB.left - containerRect.left + rectB.width / 2 + (props.endXOffset ?? 0);
    const endY = rectB.top - containerRect.top + rectB.height / 2 + (props.endYOffset ?? 0);

    // Check if the light beam is in a vertical direction (the distance in the y-direction is greater than the distance in the x-direction).
    isVertical.value = Math.abs(endY - startY) > Math.abs(endX - startX);

    // Determine the animation direction based on the position relationship between the starting point and the endpoint
    isRightToLeft.value = endX < startX;
    isBottomToTop.value = endY < startY;

    const controlY = startY - (props.curvature ?? 0);
    const d = `M ${startX},${startY} Q ${(startX + endX) / 2},${controlY} ${endX},${endY}`;
    pathD.value = d;
  }
}

onBeforeUnmount(() => {
  resizeObserver?.disconnect();
});
</script>

```

## Example

Double-sided beam.

```vue
<template>
  <ClientOnly>
    <div
      ref="containerRef"
      class="relative flex w-full items-center justify-center overflow-hidden rounded-lg border bg-background p-10 md:shadow-xl"
    >
      <div class="flex size-full flex-col items-stretch justify-between gap-10">
        <div class="flex flex-row justify-between">
          <div
            ref="div1Ref"
            class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
          >
            <Icon
              name="logos:github-icon"
              size="24"
            />
          </div>
          <div
            ref="div2Ref"
            class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
          >
            <Icon
              name="logos:google-drive"
              size="24"
            />
          </div>
        </div>
      </div>

      <AnimatedBeam
        :container-ref="containerRef"
        :from-ref="div1Ref"
        :to-ref="div2Ref"
        :curvature="-20"
        :start-y-offset="10"
        :end-y-offset="10"
      />
      <AnimatedBeam
        :container-ref="containerRef"
        :from-ref="div1Ref"
        :to-ref="div2Ref"
        :curvature="20"
        :start-y-offset="-10"
        :end-y-offset="-10"
        :reverse="true"
      />
    </div>
  </ClientOnly>
</template>

<script setup lang="ts">
import { ref } from "vue";

const containerRef = ref(null);
const div1Ref = ref(null);
const div2Ref = ref(null);
</script>

```

Vertical beam.

```vue
<template>
  <ClientOnly>
    <div
      ref="containerRef"
      class="relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-lg border bg-background p-10 md:shadow-xl"
    >
      <div class="flex size-full flex-col items-center justify-between">
        <div
          ref="div1Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
        >
          <Icon
            name="logos:github-icon"
            size="24"
          />
        </div>
        <div
          ref="div2Ref"
          class="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-2 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
        >
          <Icon
            name="logos:google-drive"
            size="24"
          />
        </div>
      </div>

      <AnimatedBeam
        :container-ref="containerRef"
        :from-ref="div1Ref"
        :to-ref="div2Ref"
      />
    </div>
  </ClientOnly>
</template>

<script setup lang="ts">
import { ref } from "vue";

const containerRef = ref(null);
const div1Ref = ref(null);
const div2Ref = ref(null);
</script>

```

## API

| Prop Name            | Type          | Default                | Description                                                                  |
| -------------------- | ------------- | ---------------------- | ---------------------------------------------------------------------------- |
| `class`              | `string`      | `""`                   | Additional CSS classes to apply to the component for customization.          |
| `containerRef`       | `HTMLElement` | N/A                    | Reference to the container element where the beam is rendered.               |
| `fromRef`            | `HTMLElement` | N/A                    | Reference to the starting element from which the beam originates.            |
| `toRef`              | `HTMLElement` | N/A                    | Reference to the ending element where the beam points to.                    |
| `curvature`          | `number`      | `0`                    | Controls the curvature of the beam; higher values create a more curved path. |
| `reverse`            | `boolean`     | `false`                | Reverses the animation direction of the beam if set to `true`.               |
| `pathColor`          | `string`      | `"gray"`               | Color of the beam's path.                                                    |
| `pathWidth`          | `number`      | `2`                    | Stroke width of the beam's path.                                             |
| `pathOpacity`        | `number`      | `0.2`                  | Opacity of the beam's path.                                                  |
| `gradientStartColor` | `string`      | `"#FFAA40"`            | Starting color of the beam's gradient animation.                             |
| `gradientStopColor`  | `string`      | `"#9C40FF"`            | Ending color of the beam's gradient animation.                               |
| `delay`              | `number`      | `0`                    | Delay before the beam's animation starts, in seconds.                        |
| `duration`           | `number`      | `Random between 4–7 s` | Duration of the beam's animation cycle, in seconds.                          |
| `startXOffset`       | `number`      | `0`                    | Horizontal offset for the beam's starting point.                             |
| `startYOffset`       | `number`      | `0`                    | Vertical offset for the beam's starting point.                               |
| `endXOffset`         | `number`      | `0`                    | Horizontal offset for the beam's ending point.                               |
| `endYOffset`         | `number`      | `0`                    | Vertical offset for the beam's ending point.                                 |

## Features

- **Dynamic Beam Drawing**: Automatically renders a beam between two specified elements on the page.
- **Smooth Animation**: Features a gradient animation that flows along the beam's path for a visually engaging effect.
- **Customizable Appearance**: Easily adjust color, width, opacity, and curvature to match your design requirements.
- **Responsive Updates**: The beam adjusts its position and size in response to window resizing and element repositioning.
- **Flexible Animation Control**: Customize the animation's duration, delay, and direction for precise timing.

## Credits

- Inspired and ported from [Magic UI Animated Beam](https://magicui.design/docs/components/animated-beam).

URL: https://inspira-ui.com/components/special-effects/border-beam

---
title: Border Beam
description: A stylish animated border beam effect with customizable size, duration, colors, and delay.
---

```vue
<template>
  <ClientOnly>
    <div
      class="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
    >
      <span
        class="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"
      >
        Border Beam
      </span>
      <BorderBeam
        :size="250"
        :duration="12"
        :delay="9"
        :border-width="2"
      />
    </div>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="border-beam" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    :class="
      cn(
        'border-beam',
        'pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]',
        '![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]',
        'after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]',
        props.class,
      )
    "
  ></div>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";
import { computed } from "vue";

interface BorderBeamProps {
  class?: string;
  size?: number;
  duration?: number;
  borderWidth?: number;
  anchor?: number;
  colorFrom?: string;
  colorTo?: string;
  delay?: number;
}

const props = withDefaults(defineProps<BorderBeamProps>(), {
  size: 200,
  duration: 15000,
  anchor: 90,
  borderWidth: 1.5,
  colorFrom: "#ffaa40",
  colorTo: "#9c40ff",
  delay: 0,
});

const durationInSeconds = computed(() => `${props.duration}s`);
const delayInSeconds = computed(() => `${props.delay}s`);
</script>

<style scoped>
.border-beam {
  --size: v-bind(size);
  --duration: v-bind(durationInSeconds);
  --anchor: v-bind(anchor);
  --border-width: v-bind(borderWidth);
  --color-from: v-bind(colorFrom);
  --color-to: v-bind(colorTo);
  --delay: v-bind(delayInSeconds);
}

.animate-border-beam::after {
  animation: border-beam-anim var(--duration) infinite linear;
}

@keyframes border-beam-anim {
  to {
    offset-distance: 100%;
  }
}
</style>

```

## API

| Prop Name     | Type     | Default     | Description                                                           |
| ------------- | -------- | ----------- | --------------------------------------------------------------------- |
| `class`       | `string` | `""`        | Additional CSS classes for custom styling.                            |
| `size`        | `number` | `200`       | Size of the animated border beam effect.                              |
| `duration`    | `number` | `15`        | Duration of the animation in seconds.                                 |
| `borderWidth` | `number` | `1.5`       | Width of the border around the beam effect.                           |
| `anchor`      | `number` | `90`        | Anchor point for the beam, determining its position along the border. |
| `colorFrom`   | `string` | `"#ffaa40"` | Starting color for the gradient of the beam.                          |
| `colorTo`     | `string` | `"#9c40ff"` | Ending color for the gradient of the beam.                            |
| `delay`       | `number` | `0`         | Delay before the animation starts, in seconds.                        |

## Features

- **Animated Border Beam**: Adds a dynamic border beam effect that animates around the border.
- **Customizable Gradient Colors**: Adjust the `colorFrom` and `colorTo` props to create a gradient effect that suits your design.
- **Flexible Animation Settings**: Control the size, duration, and delay of the animation to fine-tune the visual experience.
- **Anchor Positioning**: Use the `anchor` prop to set the starting position of the beam along the border.

## Credits

- Ported from [Magic UI](https://magicui.design/docs/components/border-beam).

URL: https://inspira-ui.com/components/special-effects/confetti

---
title: Confetti
description: A Vue component for confetti animations.
---

```vue
<template>
  <div
    class="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
  >
    <span
      class="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"
    >
      Confetti
    </span>

    <!-- Confetti component with ref -->
    <Confetti
      ref="confettiRef"
      class="absolute left-0 top-0 z-0 size-full"
      @mouseenter="fireConfetti"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const confettiRef = ref(null);

// Function to trigger confetti
function fireConfetti() {
  confettiRef.value?.fire({});
}
</script>

```

::alert{type="info"}
**Note:** This component uses `canvas-confetti` npm package as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="confetti" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install canvas-confetti
npm install -D @types/canvas-confetti
```

```bash [pnpm]
pnpm install canvas-confetti
pnpm install -D @types/canvas-confetti
```

```bash [bun]
bun add canvas-confetti
bun add -d @types/canvas-confetti
```

```bash [yarn]
yarn add canvas-confetti
yarn add --dev @types/canvas-confetti
```

::

Copy and paste the following code

::code-group

:CodeViewerTab{label="Confetti.vue" language="vue" componentName="Confetti" type="ui" id="confetti"}
:CodeViewerTab{label="ConfettiButton.vue" language="vue" componentName="ConfettiButton" type="ui" id="confetti"}
::
::

## Examples

### Basic

```vue
<template>
  <div class="relative flex h-24 w-full flex-col items-center justify-center">
    <ConfettiButton
      class="rounded-lg bg-foreground px-4 py-2 text-background transition duration-500 hover:scale-110"
    >
      Confetti 🎉
    </ConfettiButton>
  </div>
</template>

```

### Random Direction

```vue
<template>
  <div class="relative flex h-24 w-full flex-col items-center justify-center">
    <ConfettiButton
      class="rounded-lg bg-foreground px-4 py-2 text-background transition duration-500 hover:scale-110"
      :options="{
        get angle() {
          return Math.random() * 360;
        },
      }"
    >
      Random Confetti 🎉
    </ConfettiButton>
  </div>
</template>

```

### Fireworks

```vue
<template>
  <div class="relative flex h-24 w-full flex-col items-center justify-center">
    <button
      class="rounded-lg bg-foreground px-4 py-2 text-background transition duration-500 hover:scale-110"
      @click="handleClick"
    >
      Trigger Fireworks
    </button>
  </div>
</template>

<script setup lang="ts">
import confetti from "canvas-confetti";

// Function to trigger the confetti fireworks
function handleClick() {
  const duration = 5 * 1000; // 5 seconds
  const animationEnd = Date.now() + duration;
  const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };

  // Helper function to get a random value between a range
  function randomInRange(min: number, max: number) {
    return Math.random() * (max - min) + min;
  }

  const interval = window.setInterval(() => {
    const timeLeft = animationEnd - Date.now();

    if (timeLeft <= 0) {
      clearInterval(interval);
      return;
    }

    const particleCount = 50 * (timeLeft / duration);

    // Confetti from left side
    confetti({
      ...defaults,
      particleCount,
      origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
    });

    // Confetti from right side
    confetti({
      ...defaults,
      particleCount,
      origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
    });
  }, 250);
}
</script>

```

### Side Cannons

```vue
<template>
  <div class="relative flex h-24 w-full flex-col items-center justify-center">
    <button
      class="rounded-lg bg-foreground px-4 py-2 text-background transition duration-500 hover:scale-110"
      @click="handleClick"
    >
      Trigger Side Cannons
    </button>
  </div>
</template>

<script setup lang="ts">
import confetti from "canvas-confetti";

// Function to trigger the confetti side cannons
function handleClick() {
  const end = Date.now() + 3 * 1000; // 3 seconds
  const colors = ["#a786ff", "#fd8bbc", "#eca184", "#f8deb1"];

  // Frame function to trigger confetti cannons
  function frame() {
    if (Date.now() > end) return;

    // Left side confetti cannon
    confetti({
      particleCount: 2,
      angle: 60,
      spread: 55,
      startVelocity: 60,
      origin: { x: 0, y: 0.5 },
      colors: colors,
    });

    // Right side confetti cannon
    confetti({
      particleCount: 2,
      angle: 120,
      spread: 55,
      startVelocity: 60,
      origin: { x: 1, y: 0.5 },
      colors: colors,
    });

    requestAnimationFrame(frame); // Keep calling the frame function
  }

  frame();
}
</script>

```

## API

### Components props

::steps{level=4}

#### `Confetti`

| Prop Name       | Type                    | Default | Description                                                       |
| --------------- | ----------------------- | ------- | ----------------------------------------------------------------- |
| `options`       | `ConfettiOptions`       | `{}`    | Options for individual confetti bursts.                           |
| `globalOptions` | `ConfettiGlobalOptions` | `{}`    | Global options for the confetti instance (e.g., resize behavior). |
| `manualstart`   | `boolean`               | `false` | If `true`, confetti won't start automatically on mount.           |

#### `ConfettiOptions`

| Property                  | Type                        | Default                                                                         | Description                                                            |
| ------------------------- | --------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `particleCount`           | `number`                    | `50`                                                                            | The number of confetti particles to launch.                            |
| `angle`                   | `number`                    | `90`                                                                            | The angle in degrees at which to launch the confetti.                  |
| `spread`                  | `number`                    | `45`                                                                            | The spread in degrees of the confetti.                                 |
| `startVelocity`           | `number`                    | `45`                                                                            | The initial velocity of the confetti particles.                        |
| `decay`                   | `number`                    | `0.9`                                                                           | The rate at which confetti particles slow down.                        |
| `gravity`                 | `number`                    | `1`                                                                             | The gravity applied to confetti particles.                             |
| `drift`                   | `number`                    | `0`                                                                             | The horizontal drift applied to confetti particles.                    |
| `ticks`                   | `number`                    | `200`                                                                           | The number of animation frames the confetti should last.               |
| `origin`                  | `{ x: number, y: number }`  | `{ x: 0.5, y: 0.5 }`                                                            | The origin point (from 0 to 1) of the confetti emission.               |
| `colors`                  | `string[]`                  | `['#26ccff', '#a25afd', '#ff5e7e', '#88ff5a', '#fcff42', '#ffa62d', '#ff36ff']` | Array of color strings in HEX format for the confetti particles.       |
| `shapes`                  | `string[]`                  | `['square', 'circle']`                                                          | Array of shapes for the confetti particles.                            |
| `scalar`                  | `number`                    | `1`                                                                             | Scaling factor for confetti particle sizes.                            |
| `zIndex`                  | `number`                    | `100`                                                                           | The z-index value for the confetti canvas element.                     |
| `disableForReducedMotion` | `boolean`                   | `false`                                                                         | Disables confetti for users who prefer reduced motion.                 |
| `useWorker`               | `boolean`                   | `true`                                                                          | Use a Web Worker for better performance.                               |
| `resize`                  | `boolean`                   | `true`                                                                          | Whether to automatically resize the canvas when the window resizes.    |
| `canvas`                  | `HTMLCanvasElement \| null` | `null`                                                                          | Custom canvas element to draw confetti on.                             |
| `gravity`                 | `number`                    | `1`                                                                             | The gravity applied to confetti particles.                             |
| `drift`                   | `number`                    | `0`                                                                             | The horizontal drift applied to particles.                             |
| `flat`                    | `boolean`                   | `false`                                                                         | If `true`, confetti particles will be flat (no rotation or 3D effect). |

#### `ConfettiButton`

| Prop Name | Type                                               | Default | Description                                      |
| --------- | -------------------------------------------------- | ------- | ------------------------------------------------ |
| `options` | `ConfettiOptions & { canvas?: HTMLCanvasElement }` | `{}`    | Options for confetti when the button is clicked. |

::

## Features

- **Confetti Animation**: Easily add confetti animations to your Vue application.
- **Customizable Options**: Configure both global and individual options for confetti behavior.
- **Manual Control**: Choose whether to start confetti automatically or manually trigger it.
- **Button Integration**: Use the `ConfettiButton` component to trigger confetti on button clicks.

## Credits

- Built using the [canvas-confetti](https://www.npmjs.com/package/canvas-confetti) library.
- Ported from [Magic UI Confetti](https://magicui.design/docs/components/confetti).

URL: https://inspira-ui.com/components/special-effects/glow-border

---
title: Glow Border
description: An animated border effect.
---

```vue
<template>
  <ClientOnly>
    <div class="p-12 max-lg:p-4">
      <GlowBorder
        class="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
        :color="['#A07CFE', '#FE8FB5', '#FFBE7B']"
      >
        <span
          class="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-7xl font-semibold leading-none text-transparent dark:from-white dark:to-zinc-700/75"
        >
          Glow Border
        </span>
      </GlowBorder>
    </div>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="glow-border" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    :style="parentStyles"
    :class="
      cn(
        'relative grid min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white glow-border',
        $props.class,
      )
    "
  >
    <div
      :style="childStyles"
      :class="
        cn(
          `glow-border before:absolute before:inset-0 before:aspect-square before:size-full before:rounded-[--border-radius] before:bg-[length:300%_300%] before:p-[--border-width] before:opacity-50 before:will-change-[background-position] before:content-['']`,
          'before:![-webkit-mask-composite:xor] before:![mask-composite:exclude] before:[mask:--mask-linear-gradient]',
        )
      "
    ></div>
    <slot />
  </div>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";
import { computed } from "vue";

interface Props {
  borderRadius?: number;
  color?: string | Array<string>;
  borderWidth?: number;
  duration?: number;
  class?: string;
}

const props = withDefaults(defineProps<Props>(), {
  borderRadius: 10,
  color: "#FFF",
  borderWidth: 2,
  duration: 10,
});

const parentStyles = computed(() => {
  return { "--border-radius": `${props.borderRadius}px` };
});

const childStyles = computed(() => ({
  "--border-width": `${props.borderWidth}px`,
  "--border-radius": `${props.borderRadius}px`,
  "--glow-pulse-duration": `${props.duration}s`,
  "--mask-linear-gradient": `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
  "--background-radial-gradient": `radial-gradient(circle, transparent, ${
    props.color instanceof Array ? props.color.join(",") : props.color
  }, transparent)`,
}));
</script>

<style scoped>
.glow-border::before {
  animation: glow-pulse var(--glow-pulse-duration) infinite linear;
  background-image: var(--background-radial-gradient);
}

@keyframes glow-pulse {
  0% {
    background-position: 0% 0%;
  }
  50% {
    background-position: 100% 100%;
  }
  100% {
    background-position: 0% 0%;
  }
}
</style>

```

## API

| Prop Name      | Type                 | Default | Description                                                |
| -------------- | -------------------- | ------- | ---------------------------------------------------------- |
| `duration`     | `number`             | `10`    | Duration of the glowing border animation.                  |
| `color`        | `string \| string[]` | `#FFF`  | Color or array of colors to applied on the glowing border. |
| `borderRadius` | `number`             | `10`    | Radius of the border.                                      |
| `borderWidth`  | `number`             | `2`     | Width of the border.                                       |

## Credits

- Credits to [Magic UI](https://magicui.design/docs/components/shine-border) for this fantastic component.

URL: https://inspira-ui.com/components/special-effects/glowing-effect

---
title: Glowing Effect
description: A dynamic proximity-based glow effect that reacts to mouse movements and scroll events, perfect for highlighting interactive elements.
navBadges:
  - value: New
    type: lime
---

```vue
<template>
  <ul
    class="grid grid-cols-1 grid-rows-none gap-4 overflow-auto xl:max-h-[56rem] xl:grid-rows-2 lg:gap-4 md:grid-cols-12 md:grid-rows-3"
  >
    <li
      v-for="item in gridItems"
      :key="item.title"
      :class="cn('min-h-[14rem] list-none', item.area)"
    >
      <div class="rounded-2.5xl relative h-full border p-2 md:rounded-3xl md:p-3">
        <GlowingEffect
          :spread="40"
          :glow="true"
          :disabled="false"
          :proximity="64"
          :inactive-zone="0.01"
        />
        <div
          class="border-0.75 relative flex h-full flex-col justify-between gap-6 overflow-hidden rounded-xl p-6 md:p-6 dark:shadow-[0px_0px_27px_0px_#2D2D2D]"
        >
          <div class="relative flex flex-1 flex-col justify-between gap-3">
            <div class="w-fit rounded-lg border border-gray-600 p-2">
              <Icon
                class="size-4 text-black dark:text-neutral-500"
                :name="item.icon"
              ></Icon>
            </div>
            <div class="space-y-3">
              <h3
                class="-tracking-4 text-balance pt-0.5 font-sans text-xl/[1.375rem] font-semibold text-black md:text-2xl/[1.875rem] dark:text-white"
              >
                {{ item.title }}
              </h3>
              <h2
                class="font-sans text-sm/[1.125rem] text-black md:text-base/[1.375rem] dark:text-neutral-400 [&_b]:md:font-semibold [&_strong]:md:font-semibold"
              >
                {{ item.description }}
              </h2>
            </div>
          </div>
        </div>
      </div>
    </li>
  </ul>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";

const gridItems = [
  {
    area: "md:[grid-area:1/1/2/7] xl:[grid-area:1/1/2/5]",
    icon: "lucide:box",
    title: "Unbox Endless Possibilities",
    description:
      "Open up Inspira UI to discover so many features, you’ll wonder if you’ve wandered into a magical subscription for infinite goodies.",
  },
  {
    area: "md:[grid-area:1/7/2/13] xl:[grid-area:2/1/3/5]",
    icon: "lucide:settings",
    title: "Crank the Dials to Eleven",
    description:
      "We packed Inspira UI with enough customizable settings to keep you tweaking forever. If it’s broken, you probably forgot to flip one more switch!",
  },
  {
    area: "md:[grid-area:2/1/3/7] lg:[grid-area:1/5/3/8]",
    icon: "lucide:music",
    title: "Dance Your Way to Better UI",
    description:
      "Forget dull interfaces—Inspira UI brings your Vue and Nuxt apps to life with animations so smooth, you’ll wonder!",
  },
  {
    area: "md:[grid-area:2/7/3/13] xl:[grid-area:1/8/2/13]",
    icon: "lucide:sparkles",
    title: "Spark a Little Magic",
    description:
      "Make your interface shine brighter than your future. Inspira UI turns that dull design into an enchanting experience—fairy dust included!",
  },
  {
    area: "md:[grid-area:3/1/4/13] xl:[grid-area:2/8/3/13]",
    icon: "lucide:search",
    title: "Seek and You Shall Find",
    description:
      "Our search is so advanced it might unearth your lost socks. Just don’t blame us when you realize they don’t match!",
  },
];
</script>

```

## API

| Prop Name          | Type                   | Default     | Description                                                                                           |
| ------------------ | ---------------------- | ----------- | ----------------------------------------------------------------------------------------------------- |
| `blur`             | `number`               | `0`         | The blur radius applied to the glow layer.                                                            |
| `inactiveZone`     | `number`               | `0.7`       | Defines the inner radius (as a fraction of the smallest dimension) within which the glow is inactive. |
| `proximity`        | `number`               | `0`         | Additional proximity distance (in pixels) to trigger the glow when the cursor is near the element.    |
| `spread`           | `number`               | `20`        | Size of the spread of the glow effect around the element.                                             |
| `variant`          | `"default" \| "white"` | `"default"` | Variant of the glow style (e.g., a white-themed version).                                             |
| `glow`             | `boolean`              | `false`     | Controls the visibility of the static glow border.                                                    |
| `class`            | `string`               | `""`        | Additional CSS classes for custom styling.                                                            |
| `disabled`         | `boolean`              | `true`      | Disables the proximity detection and glow animations when `true`.                                     |
| `movementDuration` | `number`               | `2`         | Duration (in seconds) of the smooth rotation animation.                                               |
| `borderWidth`      | `number`               | `1`         | Width (in pixels) of the border applied to the glow effect.                                           |

## Install using CLI

```vue
<InstallationCli component-id="glowing-effect" />
```

## Install Manually

You can copy and paste the following code to create this component:

::code-group

::CodeViewerTab{label="GlowingEffect.vue" language="vue" componentName="GlowingEffect" type="ui" id="glowing-effect"}
::

::

## Features

- **Proximity-Based Activation**: The glow effect is only active when the cursor is within a certain distance of the element.
- **Smooth Angle Animation**: Gradually animates rotation based on pointer movement for an appealing dynamic glow.
- **Configurable Glow Properties**: Fine-tune blur, spread, and proximity to achieve various visual effects.
- **Variant Support**: Choose between default or white glow styling.
- **Performance Optimizations**: Event listeners and animation frames are managed efficiently.

## Credits

- Ported from (Aceternity UI Glowing Effect)[https://ui.aceternity.com/components/glowing-effect]

URL: https://inspira-ui.com/components/special-effects/meteors

---
title: Meteor
description: A component that displays a meteor shower animation with customizable meteor count and styling.
---

```vue
<template>
  <ClientOnly>
    <div class="flex w-full flex-col items-center justify-center py-24">
      <div class="relative w-full max-w-xs">
        <div
          class="absolute inset-0 size-full scale-[0.80] rounded-full bg-red-500 bg-gradient-to-r from-blue-500 to-teal-500 blur-3xl"
        />
        <div
          class="relative flex h-full flex-col items-start justify-end overflow-hidden rounded-2xl border border-gray-800 bg-gray-900 px-4 py-8 shadow-xl"
        >
          <div
            class="mb-4 flex size-5 items-center justify-center rounded-full border border-gray-500"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              strokeWidth="1.5"
              stroke="currentColor"
              class="size-2 text-gray-300"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25"
              />
            </svg>
          </div>

          <h1 class="relative z-50 mb-4 text-xl font-bold text-white">
            Meteors because they&apos;re cool
          </h1>

          <p class="relative z-50 mb-4 text-base font-normal text-slate-500">
            I don&apos;t know what to write so I&apos;ll just paste something cool here. One more
            sentence because lorem ipsum is just unacceptable. Won&apos;t ChatGPT the shit out of
            this.
          </p>

          <button class="rounded-lg border border-gray-500 px-4 py-1 text-gray-300">Explore</button>
          <Meteors />
        </div>
      </div>
    </div>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="meteors" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <span
    v-for="index in count"
    :key="'meteor ' + index"
    :class="
      cn(
        'animate-meteor absolute top-1/2 left-1/2 h-0.5 w-0.5 rounded-[9999px] bg-slate-500 shadow-[0_0_0_1px_#ffffff10] rotate-[215deg]',
        `before:content-[''] before:absolute before:top-1/2 before:transform before:-translate-y-[50%] before:w-[50px] before:h-[1px] before:bg-gradient-to-r before:from-[#64748b] before:to-transparent`,
        $props.class,
      )
    "
    :style="{
      top: 0,
      left: Math.floor(Math.random() * (400 - -400) + -400) + 'px',
      animationDelay: Math.random() * (0.8 - 0.2) + 0.2 + 's',
      animationDuration: Math.floor(Math.random() * (10 - 2) + 2) + 's',
    }"
  ></span>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";

defineProps({
  count: {
    type: Number,
    default: 20,
  },
  class: String,
});
</script>

<style scoped>
@keyframes meteor {
  0% {
    transform: rotate(215deg) translateX(0);
    opacity: 1;
  }
  70% {
    opacity: 1;
  }
  100% {
    transform: rotate(215deg) translateX(-500px);
    opacity: 0;
  }
}

.animate-meteor {
  animation: meteor 5s linear infinite;
}
</style>

```

## API

| Prop Name | Type     | Default | Description                                                       |
| --------- | -------- | ------- | ----------------------------------------------------------------- |
| `count`   | `number` | `20`    | The number of meteors to display in the animation.                |
| `class`   | `string` | `""`    | Additional CSS classes to apply to the meteors for customization. |

## Features

- **Meteor Shower Animation**: The component renders an animated meteor shower effect, adding a dynamic visual element to your application.

- **Customizable Meteor Count**: Adjust the number of meteors by setting the `count` prop to control the density of the meteor shower.

- **Randomized Animations**: Each meteor has randomized position, delay, and duration, creating a natural and varied animation.

- **Styling Flexibility**: Pass additional CSS classes through the `class` prop to customize the appearance of the meteors.

- **Responsive Design**: The component adapts to different screen sizes and uses Vue’s reactivity for smooth animations.

## Credits

- Ported from [Aceternity UI's Meteor Effect](https://ui.aceternity.com/components/meteors)

URL: https://inspira-ui.com/components/special-effects/neon-border

---
title: Neon Border
description: A visually appealing neon border component with customizable animations and colors.
navBadges:
  - value: New
    type: lime
---

```vue
<template>
  <div class="flex h-96 w-full flex-col items-center justify-center gap-16">
    <NeonBorder :animation-type="'none'">
      <input
        type="text"
        class="size-full rounded-lg px-4 text-sm"
        placeholder="No animation"
      />
    </NeonBorder>

    <NeonBorder>
      <input
        type="text"
        class="size-full rounded-lg px-4 text-sm"
        placeholder="Half border animation"
      />
    </NeonBorder>

    <NeonBorder :animation-type="'full'">
      <input
        type="text"
        class="size-full rounded-lg px-4 text-sm"
        placeholder="Full border animation"
      />
    </NeonBorder>
  </div>
</template>

```

## API

| Prop Name       | Type                         | Default     | Description                                     |
| --------------- | ---------------------------- | ----------- | ----------------------------------------------- |
| `color1`        | `string`                     | `"#0496ff"` | Primary color of the neon border.               |
| `color2`        | `string`                     | `"#ff0a54"` | Secondary color of the neon border.             |
| `animationType` | `"none" \| "half" \| "full"` | `"half"`    | Type of animation effect applied to the border. |
| `duration`      | `number`                     | `6`         | Duration of the animation effect in seconds.    |
| `class`         | `string`                     | `""`        | Additional CSS classes for styling.             |

## Install using CLI

```vue
<InstallationCli component-id="neon-border" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group
::CodeViewerTab{label="NeonBorder.vue" language="vue" componentName="NeonBorder" type="ui" id="neon-border"}
::
::

## Features

- **Customizable Colors**: Allows setting primary and secondary neon colors.
- **Three Animation Modes**: Supports `none`, `half`, and `full` animation effects.
- **Adjustable Animation Duration**: Duration can be fine-tuned for different effects.
- **Reactive Design**: Uses Vue’s reactivity system for dynamic updates.
- **Scoped Styles**: Ensures styles do not interfere with other components.

## Credits

- Inspired by modern neon border effects.

URL: https://inspira-ui.com/components/special-effects/particle-image

---
title: Particle Image
description: Visually appealing particle animation applied to images as seen on NuxtLabs.com
---

```vue
<template>
  <div class="flex flex-col items-center justify-center">
    <ParticleImage
      image-src="/og-image.png"
      :responsive-width="true"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="particle-image" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="ParticleImage.vue" language="vue" componentName="ParticleImage" type="ui" id="particle-image"}
:CodeViewerTab{label="inspiraImageParticles.js" icon="vscode-icons:file-type-js" componentName="inspiraImageParticles" type="ui" id="particle-image" extension="js"}
:CodeViewerTab{label="inspiraImageParticles.d.ts" icon="vscode-icons:file-type-typescriptdef" componentName="inspiraImageParticles" type="ui" id="particle-image" extension="d.ts"}

::

## API

| Prop Name         | Type                                                                          | Default  | Description                                                                     |
| ----------------- | ----------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------- |
| `imageSrc`        | `string`                                                                      | `null`   | Source URL for the image to which the particle effect is applied.               |
| `class`           | `string`                                                                      | `null`   | Additional CSS classes to apply to the image element.                           |
| `canvasWidth`     | `string`                                                                      | `null`   | Width of the particle effect canvas.                                            |
| `canvasHeight`    | `string`                                                                      | `null`   | Height of the particle effect canvas.                                           |
| `gravity`         | `string`                                                                      | `null`   | Gravity force affecting the particle movement.                                  |
| `particleSize`    | `string`                                                                      | `null`   | Size of the particles.                                                          |
| `particleGap`     | `string`                                                                      | `null`   | Gap between particles.                                                          |
| `mouseForce`      | `string`                                                                      | `null`   | Force applied to particles based on mouse movement.                             |
| `renderer`        | `"default" \| "webgl"`                                                        | `null`   | The renderer to use for particle generation, either default or WebGL.           |
| `color`           | `string`                                                                      | `#FFF`   | Hexadecimal color code used for particles. Supports 3 or 6 character hex codes. |
| `colorArr`        | `number[]`                                                                    | `null`   | Array of numbers to define multiple particle colors.                            |
| `initPosition`    | `"random" \| "top" \| "left" \| "bottom" \| "right" \| "misplaced" \| "none"` | `random` | Initial position of the particles when the animation starts.                    |
| `initDirection`   | `"random" \| "top" \| "left" \| "bottom" \| "right" \| "none"`                | `random` | Initial direction of the particles when the animation starts.                   |
| `fadePosition`    | `"explode" \| "top" \| "left" \| "bottom" \| "right" \| "random" \| "none"`   | `none`   | Position where the particles fade out.                                          |
| `fadeDirection`   | `"random" \| "top" \| "left" \| "bottom" \| "right" \| "none"`                | `none`   | Direction in which the particles fade out.                                      |
| `noise`           | `number`                                                                      | `null`   | Noise of the particles.                                                         |
| `responsiveWidth` | `boolean`                                                                     | `false`  | Should the canvas be responsive.                                                |

## Credits

- Credits to [Nuxt Labs](https://nuxtlabs.com) for this inspiration.
- Credits to [NextParticles](https://nextparticle.nextco.de) for the base of the animation library.

URL: https://inspira-ui.com/components/special-effects/scratch-to-reveal

---
title: Scratch To Reveal
description: The ScratchToReveal component creates an interactive scratch-off effect with customizable dimensions and animations, revealing hidden content beneath.
---

```vue
<template>
  <ScratchToReveal
    :width="250"
    :height="250"
    :min-scratch-percentage="50"
    class="mx-auto flex items-center justify-center overflow-hidden rounded-2xl border-2 bg-gray-100"
    :gradient-colors="['#A97CF8', '#F38CB8', '#FDCC92']"
    @complete="handleComplete"
  >
    <div class="text-8xl">🥳</div>
  </ScratchToReveal>
</template>

<script lang="ts" setup>
function handleComplete() {
  // when scratch is completed do something
}
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="scratch-to-reveal" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <Motion
    ref="containerRef"
    :class="cn('relative select-none', props.class)"
    :style="{
      width: containerWidth,
      height: containerHeight,
      cursor: cursorImg,
    }"
    :initial="{
      scale: 1,
      rotate: [0, 10, -10, 10, -10, 0],
    }"
    :transition="{ duration: 0.5 }"
  >
    <canvas
      ref="canvasRef"
      :width="width"
      :height="height"
      class="absolute left-0 top-0"
      @mousedown="handleMouseDown"
      @touchstart="handleTouchStart"
    />

    <slot />
  </Motion>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { Motion, useAnimate } from "motion-v";
import { ref, computed, onMounted, onUnmounted, type Ref } from "vue";

const cursorImg =
  "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+'), auto";

interface Props {
  class?: string;
  width: number;
  height: number;
  minScratchPercentage?: number;
  gradientColors?: [string, string, string];
}

const canvasRef = ref<HTMLCanvasElement>();

const props = withDefaults(defineProps<Props>(), {
  gradientColors: () => ["#A97CF8", "#F38CB8", "#FDCC92"],
  minScratchPercentage: 50,
});

const containerWidth = computed(() => props.width + "px");
const containerHeight = computed(() => props.height + "px");

const context = ref<CanvasRenderingContext2D>();

const emit = defineEmits<{
  complete: [];
}>();

const isScratching = ref(false);
const isComplete = ref(false);

function handleMouseDown() {
  isScratching.value = true;
}
function handleTouchStart() {
  isScratching.value = true;
}

const canvasWidth = computed(() => canvasRef.value?.width || props.width);
const canvasHeight = computed(() => canvasRef.value?.height || props.height);

function drawCanvas(canvasRef: Ref<HTMLCanvasElement>) {
  context.value = canvasRef.value.getContext("2d")!;
  context.value.fillStyle = "#ccc";
  context.value.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
  const gradient = context.value.createLinearGradient(0, 0, canvasWidth.value, canvasHeight.value);
  gradient.addColorStop(0, props.gradientColors[0]);
  gradient.addColorStop(0.5, props.gradientColors[1]);
  gradient.addColorStop(1, props.gradientColors[2]);
  context.value.fillStyle = gradient;
  context.value.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
}

function scratch(clientX: number, clientY: number) {
  if (canvasRef.value && context.value) {
    const rect = canvasRef.value.getBoundingClientRect();
    const x = clientX - rect.left + 16;
    const y = clientY - rect.top + 16;

    context.value.globalCompositeOperation = "destination-out";
    context.value.beginPath();
    context.value.arc(x, y, 30, 0, Math.PI * 2);
    context.value.fill();
  }
}

function handleDocumentMouseMove(event: MouseEvent) {
  if (!isScratching.value) return;
  scratch(event.clientX, event.clientY);
}

function handleDocumentTouchMove(event: TouchEvent) {
  if (!isScratching.value) return;
  const touch = event.touches[0];
  scratch(touch.clientX, touch.clientY);
}

function handleDocumentMouseUp() {
  isScratching.value = false;
  checkCompletion();
}
function handleDocumentTouchEnd() {
  isScratching.value = false;
  checkCompletion();
}

function addEventListeners() {
  document.addEventListener("mousedown", handleDocumentMouseMove);
  document.addEventListener("mousemove", handleDocumentMouseMove);
  document.addEventListener("touchstart", handleDocumentTouchMove);
  document.addEventListener("touchmove", handleDocumentTouchMove);
  document.addEventListener("mouseup", handleDocumentMouseUp);
  document.addEventListener("touchend", handleDocumentTouchEnd);
  document.addEventListener("touchcancel", handleDocumentTouchEnd);
}

function checkCompletion() {
  if (isComplete.value) return;

  if (canvasRef.value && context.value) {
    const imageData = context.value.getImageData(0, 0, canvasWidth.value, canvasHeight.value);
    const pixels = imageData.data;
    const totalPixels = pixels.length / 4;
    let clearPixels = 0;

    for (let i = 3; i < pixels.length; i += 4) {
      if (pixels[i] === 0) {
        clearPixels++;
      }
    }

    const percentage = (clearPixels / totalPixels) * 100;

    if (percentage >= props.minScratchPercentage) {
      isComplete.value = true;
      context.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);

      startAnimation();
    } else {
      isScratching.value = false;
    }
  }
}

const [containerRef, animate] = useAnimate();
async function startAnimation() {
  if (!containerRef.value) return;
  animate(containerRef.value, {
    scale: 1,
    rotate: [0, 10, -10, 10, -10, 0],
  });

  emit("complete");
}

onMounted(() => {
  if (!canvasRef.value) return;

  drawCanvas(canvasRef as Ref<HTMLCanvasElement>);

  addEventListeners();
});

function removeEventListeners() {
  document.removeEventListener("mousedown", handleDocumentMouseMove);
  document.removeEventListener("mousemove", handleDocumentMouseMove);
  document.removeEventListener("touchstart", handleDocumentTouchMove);
  document.removeEventListener("touchmove", handleDocumentTouchMove);
  document.removeEventListener("mouseup", handleDocumentMouseUp);
  document.removeEventListener("touchend", handleDocumentTouchEnd);
  document.removeEventListener("touchcancel", handleDocumentTouchEnd);
}
onUnmounted(() => {
  removeEventListeners();
});
</script>

<style></style>

```

## API

| Prop Name              | Type                     | Default | Description                                                                                   |
| ---------------------- | ------------------------ | ------- | --------------------------------------------------------------------------------------------- |
| `class`                | `string`                 | `""`    | The class name to apply to the component.                                                     |
| `width`                | `number`                 | `""`    | Width of the scratch container.                                                               |
| `height`               | `number`                 | `""`    | Height of the scratch container.                                                              |
| `minScratchPercentage` | `number`                 | `50`    | Minimum percentage of scratched area to be considered as completed (Value between 0 and 100). |
| `gradientColors`       | `[string,string,string]` | `-`     | Gradient colors for the scratch effect.                                                       |

| Event Name | Payload | Description                                        |
| ---------- | ------- | -------------------------------------------------- |
| `complete` | `-`     | Callback function called when scratch is completed |

| Slot Name | Default Content | Description                            |
| --------- | --------------- | -------------------------------------- |
| `default` | `-`             | The text below the scratch-off ticket. |

## Component Code

You can copy and paste the following code to create this component:

```vue
<template>
  <Motion
    ref="containerRef"
    :class="cn('relative select-none', props.class)"
    :style="{
      width: containerWidth,
      height: containerHeight,
      cursor: cursorImg,
    }"
    :initial="{
      scale: 1,
      rotate: [0, 10, -10, 10, -10, 0],
    }"
    :transition="{ duration: 0.5 }"
  >
    <canvas
      ref="canvasRef"
      :width="width"
      :height="height"
      class="absolute left-0 top-0"
      @mousedown="handleMouseDown"
      @touchstart="handleTouchStart"
    />

    <slot />
  </Motion>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { Motion, useAnimate } from "motion-v";
import { ref, computed, onMounted, onUnmounted, type Ref } from "vue";

const cursorImg =
  "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+'), auto";

interface Props {
  class?: string;
  width: number;
  height: number;
  minScratchPercentage?: number;
  gradientColors?: [string, string, string];
}

const canvasRef = ref<HTMLCanvasElement>();

const props = withDefaults(defineProps<Props>(), {
  gradientColors: () => ["#A97CF8", "#F38CB8", "#FDCC92"],
  minScratchPercentage: 50,
});

const containerWidth = computed(() => props.width + "px");
const containerHeight = computed(() => props.height + "px");

const context = ref<CanvasRenderingContext2D>();

const emit = defineEmits<{
  complete: [];
}>();

const isScratching = ref(false);
const isComplete = ref(false);

function handleMouseDown() {
  isScratching.value = true;
}
function handleTouchStart() {
  isScratching.value = true;
}

const canvasWidth = computed(() => canvasRef.value?.width || props.width);
const canvasHeight = computed(() => canvasRef.value?.height || props.height);

function drawCanvas(canvasRef: Ref<HTMLCanvasElement>) {
  context.value = canvasRef.value.getContext("2d")!;
  context.value.fillStyle = "#ccc";
  context.value.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
  const gradient = context.value.createLinearGradient(0, 0, canvasWidth.value, canvasHeight.value);
  gradient.addColorStop(0, props.gradientColors[0]);
  gradient.addColorStop(0.5, props.gradientColors[1]);
  gradient.addColorStop(1, props.gradientColors[2]);
  context.value.fillStyle = gradient;
  context.value.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
}

function scratch(clientX: number, clientY: number) {
  if (canvasRef.value && context.value) {
    const rect = canvasRef.value.getBoundingClientRect();
    const x = clientX - rect.left + 16;
    const y = clientY - rect.top + 16;

    context.value.globalCompositeOperation = "destination-out";
    context.value.beginPath();
    context.value.arc(x, y, 30, 0, Math.PI * 2);
    context.value.fill();
  }
}

function handleDocumentMouseMove(event: MouseEvent) {
  if (!isScratching.value) return;
  scratch(event.clientX, event.clientY);
}

function handleDocumentTouchMove(event: TouchEvent) {
  if (!isScratching.value) return;
  const touch = event.touches[0];
  scratch(touch.clientX, touch.clientY);
}

function handleDocumentMouseUp() {
  isScratching.value = false;
  checkCompletion();
}
function handleDocumentTouchEnd() {
  isScratching.value = false;
  checkCompletion();
}

function addEventListeners() {
  document.addEventListener("mousedown", handleDocumentMouseMove);
  document.addEventListener("mousemove", handleDocumentMouseMove);
  document.addEventListener("touchstart", handleDocumentTouchMove);
  document.addEventListener("touchmove", handleDocumentTouchMove);
  document.addEventListener("mouseup", handleDocumentMouseUp);
  document.addEventListener("touchend", handleDocumentTouchEnd);
  document.addEventListener("touchcancel", handleDocumentTouchEnd);
}

function checkCompletion() {
  if (isComplete.value) return;

  if (canvasRef.value && context.value) {
    const imageData = context.value.getImageData(0, 0, canvasWidth.value, canvasHeight.value);
    const pixels = imageData.data;
    const totalPixels = pixels.length / 4;
    let clearPixels = 0;

    for (let i = 3; i < pixels.length; i += 4) {
      if (pixels[i] === 0) {
        clearPixels++;
      }
    }

    const percentage = (clearPixels / totalPixels) * 100;

    if (percentage >= props.minScratchPercentage) {
      isComplete.value = true;
      context.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);

      startAnimation();
    } else {
      isScratching.value = false;
    }
  }
}

const [containerRef, animate] = useAnimate();
async function startAnimation() {
  if (!containerRef.value) return;
  animate(containerRef.value, {
    scale: 1,
    rotate: [0, 10, -10, 10, -10, 0],
  });

  emit("complete");
}

onMounted(() => {
  if (!canvasRef.value) return;

  drawCanvas(canvasRef as Ref<HTMLCanvasElement>);

  addEventListeners();
});

function removeEventListeners() {
  document.removeEventListener("mousedown", handleDocumentMouseMove);
  document.removeEventListener("mousemove", handleDocumentMouseMove);
  document.removeEventListener("touchstart", handleDocumentTouchMove);
  document.removeEventListener("touchmove", handleDocumentTouchMove);
  document.removeEventListener("mouseup", handleDocumentMouseUp);
  document.removeEventListener("touchend", handleDocumentTouchEnd);
  document.removeEventListener("touchcancel", handleDocumentTouchEnd);
}
onUnmounted(() => {
  removeEventListeners();
});
</script>

<style></style>

```

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Inspired by [MagicUI Scratch To Reveal](https://magicui.design/docs/components/scratch-to-reveal).

URL: https://inspira-ui.com/components/text-animations/3d-text

---
title: 3D Text
description: A stylish 3D text component with customizable colors, shadows, and animation options.
---

```vue
<template>
  <div
    class="flex flex-col items-center justify-center overflow-hidden bg-black px-4 py-16 font-heading"
  >
    <Text3d
      class="text-8xl font-bold max-md:text-7xl"
      shadow-color="red"
    >
      3D
    </Text3d>
    <Text3d
      class="text-8xl font-black max-md:text-7xl"
      shadow-color="skyblue"
      :animate="false"
    >
      IS
    </Text3d>
    <Text3d class="text-8xl font-bold max-md:text-7xl">AWESOME</Text3d>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="text-3d" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    :class="
      cn('text-3d flex items-center justify-center', animate ? 'animate-text-3d' : '', props.class)
    "
  >
    <slot></slot>
  </div>
</template>

<script lang="ts" setup>
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

interface Props {
  textColor?: string;
  letterSpacing?: number;
  strokeColor?: string;
  shadowColor?: string;
  strokeSize?: number;
  shadow1Size?: number;
  shadow2Size?: number;
  class?: HTMLAttributes["class"];
  animate?: boolean;
  animationDuration?: number;
}

const props = withDefaults(defineProps<Props>(), {
  textColor: "white",
  letterSpacing: -0.1,
  strokeColor: "black",
  shadowColor: "yellow",
  strokeSize: 20,
  shadow1Size: 7,
  shadow2Size: 10,
  animate: true,
  animationDuration: 1500,
});

const letterSpacingInCh = computed(() => {
  return `${props.letterSpacing}ch`;
});

const strokeSizeInPx = computed(() => {
  return `${props.strokeSize}px`;
});

const shadow1SizeInPx = computed(() => {
  return `${props.shadow1Size}px`;
});

const shadow2SizeInPx = computed(() => {
  return `${props.shadow2Size}px`;
});

const animationDurationInMs = computed(() => {
  return `${props.animationDuration}ms`;
});
</script>

<style scoped>
.text-3d {
  paint-order: stroke fill;
  letter-spacing: v-bind(letterSpacingInCh);
  -webkit-text-stroke: v-bind(strokeSizeInPx) v-bind(strokeColor);
  text-shadow:
    v-bind(shadow1SizeInPx) v-bind(shadow1SizeInPx) 0px v-bind(strokeColor),
    v-bind(shadow2SizeInPx) v-bind(shadow2SizeInPx) 0px v-bind(shadowColor);
  color: v-bind(textColor);
}

.animate-text-3d {
  animation: wiggle v-bind(animationDurationInMs) ease-in-out infinite alternate;
  animation-timing-function: ease-in-out;
  transform-origin: center;
}

@keyframes wiggle {
  0% {
    transform: rotate(0deg);
  }
  12% {
    transform: rotate(5deg);
  }
  25% {
    transform: rotate(-5deg);
  }
  38% {
    transform: rotate(3deg);
  }
  50% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(0deg);
  }
}
</style>

```

## API

| Prop Name           | Type      | Default    | Description                                        |
| ------------------- | --------- | ---------- | -------------------------------------------------- |
| `textColor`         | `string`  | `"white"`  | Color of the main text.                            |
| `letterSpacing`     | `number`  | `-0.1`     | Adjusts the spacing between letters in `ch` units. |
| `strokeColor`       | `string`  | `"black"`  | Color of the text stroke.                          |
| `shadowColor`       | `string`  | `"yellow"` | Color of the text shadow.                          |
| `strokeSize`        | `number`  | `20`       | Thickness of the text stroke in pixels.            |
| `shadow1Size`       | `number`  | `7`        | Size of the first shadow layer in pixels.          |
| `shadow2Size`       | `number`  | `10`       | Size of the second shadow layer in pixels.         |
| `class`             | `string`  | `""`       | Additional CSS classes for custom styling.         |
| `animate`           | `boolean` | `true`     | Enables wiggle animation when set to `true`.       |
| `animationDuration` | `number`  | `1500`     | Duration of the wiggle animation in milliseconds.  |

## Features

- **3D Text Effect**: Adds a three-dimensional stroke and shadow effect to text for a bold, layered look.
- **Customizable Colors & Sizes**: Easily adjust text color, stroke size, shadow colors, and letter spacing.
- **Wiggle Animation**: Includes an optional wiggle animation to make the text bounce for added emphasis.
- **Flexible Animation Control**: Customize the animation speed with the `animationDuration` prop.

URL: https://inspira-ui.com/components/text-animations/blur-reveal

---
title: Blur Reveal
description: A component to smoothly blur fade in content.
---

```vue
<template>
  <ClientOnly>
    <BlurReveal
      :delay="0.2"
      :duration="0.75"
      class="p-8"
    >
      <h2 class="text-3xl font-bold tracking-tighter xl:text-6xl/none sm:text-5xl">Hey there 👋</h2>
      <span class="text-pretty text-xl tracking-tighter xl:text-4xl/none sm:text-3xl">
        How is it going?
      </span>
    </BlurReveal>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="blur-reveal" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    ref="container"
    :class="props.class"
  >
    <Motion
      v-for="(child, index) in children"
      :key="index"
      ref="childElements"
      as="div"
      :initial="getInitial()"
      :in-view="getAnimate()"
      :transition="{
        duration: props.duration,
        easing: 'easeInOut',
        delay: props.delay * index,
      }"
    >
      <component :is="child" />
    </Motion>
  </div>
</template>

<script setup lang="ts">
import { Motion } from "motion-v";
import { ref, onMounted, watchEffect, useSlots } from "vue";

interface Props {
  duration?: number;
  delay?: number;
  blur?: string;
  yOffset?: number;
  class?: string;
}

const props = withDefaults(defineProps<Props>(), {
  duration: 1,
  delay: 2,
  blur: "20px",
  yOffset: 20,
});

const container = ref(null);
const childElements = ref([]);
const slots = useSlots();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const children = ref<any>([]);

onMounted(() => {
  // This will reactively capture all content provided in the default slot
  watchEffect(() => {
    children.value = slots.default ? slots.default() : [];
  });
});

function getInitial() {
  return {
    opacity: 0,
    filter: `blur(${props.blur})`,
    y: props.yOffset,
  };
}

function getAnimate() {
  return {
    opacity: 1,
    filter: `blur(0px)`,
    y: 0,
  };
}
</script>

```

## API

| Prop Name  | Type     | Default | Description                                                                  |
| ---------- | -------- | ------- | ---------------------------------------------------------------------------- |
| `duration` | `number` | `1`     | Duration of the blur fade in animation.                                      |
| `delay`    | `number` | `1`     | Delay between child components to reveal                                     |
| `blur`     | `string` | `10px`  | Amount of blur to apply to the child components.                             |
| `yOffset`  | `number` | `20`    | Specifies the vertical offset distance (yOffset) for the entrance animation. |

## Credits

- Credits to [Magic UI](https://magicui.design/docs/components/blur-fade) for this fantastic component.

URL: https://inspira-ui.com/components/text-animations/box-reveal

---
title: Box Reveal
description: An animated box reveal effect with customizable colors, duration, and delay.
---

```vue
<template>
  <div class="size-full max-w-lg items-center justify-center overflow-hidden p-8">
    <BoxReveal color="#E1251B">
      <p class="text-[3.5rem] font-semibold">Inspira UI<span class="text-[#E1251B]">.</span></p>
    </BoxReveal>

    <BoxReveal
      color="#E1251B"
      :duration="0.8"
    >
      <h2 class="mt-[.5rem] text-[1rem]">
        Beautiful components for
        <span class="text-[#E1251B]">Vue &amp; Nuxt.</span>
      </h2>
    </BoxReveal>

    <BoxReveal
      color="#E1251B"
      :duration="1"
    >
      <div class="mt-6">
        <p>
          -&gt; Free and open-source animated components built with
          <span class="font-semibold text-[#E1251B]"> Vue/Nuxt</span>,
          <span class="font-semibold text-[#E1251B]"> Typescript</span>,
          <span class="font-semibold text-[#E1251B]"> Tailwind CSS</span>, and
          <span class="font-semibold text-[#E1251B]"> motion-v</span>
          . <br />
          -&gt; 100% open-source, and customizable. <br />
        </p>
      </div>
    </BoxReveal>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="box-reveal" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div :class="cn('relative', $props.class)">
    <Motion
      :initial="initialMainVariants"
      :in-view="visibleMainVariants"
      :transition="{
        duration: props.duration,
        delay: props.delay * 2,
      }"
    >
      <slot />
    </Motion>
    <Motion
      class="box-background absolute inset-0 z-20"
      :initial="initialSlideVariants"
      :in-view="visibleSlideVariants"
      :transition="{
        duration: props.duration,
        ease: 'easeIn',
        delay: props.delay,
      }"
    ></Motion>
  </div>
</template>

<script lang="ts" setup>
import { Motion } from "motion-v";
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

interface BoxRevealProps {
  color?: string;
  duration?: number;
  class?: HTMLAttributes["class"];
  delay?: number;
}

const props = withDefaults(defineProps<BoxRevealProps>(), {
  color: "#5046e6",
  duration: 0.5,
  delay: 0.25,
});

// Motion variants
const initialMainVariants = { opacity: 0, y: 25 };
const visibleMainVariants = {
  opacity: 1,
  y: 0,
};

const initialSlideVariants = { left: "0%" };
const visibleSlideVariants = {
  left: "100%",
};
</script>

<style scoped>
.box-background {
  background: v-bind(color);
}
</style>

```

## API

| Prop Name  | Type     | Default     | Description                                          |
| ---------- | -------- | ----------- | ---------------------------------------------------- |
| `color`    | `string` | `"#5046e6"` | Background color of the reveal box.                  |
| `duration` | `number` | `0.5`       | Duration of the reveal animation in seconds.         |
| `delay`    | `number` | `0.25`      | Delay before the reveal animation starts in seconds. |
| `class`    | `string` | `""`        | Additional CSS classes for custom styling.           |

## Features

- **Box Reveal Animation**: Creates a sliding box reveal effect with smooth transitions.
- **Customizable Animation**: Control the animation timing with the `duration` and `delay` props.
- **Slot-Based Content**: Supports default slot content that appears once the reveal animation completes.
- **Custom Background Color**: Easily customize the box's background color using the `color` prop.

## Credits

- Ported from [Magic UI Box Reveal](https://magicui.design/docs/components/box-reveal).

URL: https://inspira-ui.com/components/text-animations/colorful-text

---
title: Colourful Text
description: A text component with various colours, filter and scale effects.
---

```vue
<template>
  <div class="relative flex h-screen w-full items-center justify-center overflow-hidden bg-black">
    <Motion
      as="img"
      src="https://assets.aceternity.com/linear-demo.webp"
      class="pointer-events-none absolute inset-0 size-full object-cover [mask-image:radial-gradient(circle,transparent,black_80%)]"
      :initial="{ opacity: 0 }"
      :in-view="{
        opacity: 0.5,
      }"
      :transition="{
        duration: 1,
      }"
    />
    <div>
      <h1
        class="z-2 relative text-center font-sans text-2xl font-bold text-white lg:text-7xl md:text-5xl"
      >
        The best <ColourfulText text="components" />
        <br />
        you will ever find
      </h1>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Motion } from "motion-v";
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="colourful-text" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    v-if="visibility"
    class="inline-block whitespace-pre font-sans tracking-tight"
  >
    <Motion
      v-for="(char, index) in props.text"
      :key="`${char}-${count}-${index}`"
      as-child
      :initial="{
        y: -3,
        opacity: 0.2,
        color: props.startColor,
        scale: 1,
        filter: 'blur(5px)',
      }"
      :transition="{
        duration: props.duration,
        delay: index * 0.05,
      }"
      :animate="{
        y: 0,
        opacity: 1,
        scale: 1.01,
        filter: 'blur(0px)',
        color: currentColors[index % currentColors.length],
      }"
      :exit="{
        y: -3,
        opacity: 1,
        scale: 1,
        filter: 'blur(5px)',
        color: props.startColor,
      }"
    >
      {{ char }}
    </Motion>
  </div>
</template>

<script setup lang="ts">
import { Motion } from "motion-v";
import { ref, onMounted, onUnmounted } from "vue";

interface Props {
  text: string;
  colors?: string[];
  startColor?: string;
  duration?: number;
}

const props = withDefaults(defineProps<Props>(), {
  startColor: "rgb(255,255,255)",
  duration: 0.5,
  colors: () => [
    "rgb(131, 179, 32)",
    "rgb(47, 195, 106)",
    "rgb(42, 169, 210)",
    "rgb(4, 112, 202)",
    "rgb(107, 10, 255)",
    "rgb(183, 0, 218)",
    "rgb(218, 0, 171)",
    "rgb(230, 64, 92)",
    "rgb(232, 98, 63)",
    "rgb(249, 129, 47)",
  ],
});

const currentColors = ref(props.colors);
const count = ref(0);
const visibility = ref(true);

// eslint-disable-next-line no-undef
let intervalId: undefined | NodeJS.Timeout = undefined;
onMounted(() => {
  document.addEventListener("visibilitychange", handleVisibilityChange);
  handleVisibilityChange();

  intervalId = setInterval(() => {
    const shuffled = [...props.colors].sort(() => 0.5 - Math.random());
    currentColors.value = shuffled;
    count.value++;
  }, 5000);
});

function handleVisibilityChange() {
  visibility.value = document.visibilityState === "visible";
}

onUnmounted(() => {
  document.removeEventListener("visibilitychange", handleVisibilityChange);
  clearInterval(intervalId);
});
</script>

<style scoped></style>

```

## API

| Prop Name    | Type       | Default                                                                                                                                                                                                            | Description                                                                                                                                               |
| ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`       | `string`   | `"black"`                                                                                                                                                                                                          | The text string to be rendered with colorful animated characters. Each character will be individually animated with color transitions and motion effects. |
| `colors`     | `string[]` | `[ "rgb(131, 179, 32)", "rgb(47, 195, 106)", "rgb(42, 169, 210)", "rgb(4, 112, 202)", "rgb(107, 10, 255)", "rgb(183, 0, 218)", "rgb(218, 0, 171)", "rgb(230, 64, 92)", "rgb(232, 98, 63)", "rgb(249, 129, 47)", ]` | The text use colors.                                                                                                                                      |
| `startColor` | `string`   | `"rgb(255,255,255)"`                                                                                                                                                                                               | The char start color.                                                                                                                                     |
| `duration`   | `number`   | `5`                                                                                                                                                                                                                | The animation duration time in seconds.                                                                                                                   |

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Ported from [Aceternity UI Colourful Text](https://ui.aceternity.com/components/colourful-text)

URL: https://inspira-ui.com/components/text-animations/flip-words

---
title: Flip Words
description: A component that flips through a list of words.
---

```vue
<template>
  <div class="flex h-[40rem] items-center justify-center px-4">
    <div class="mx-auto text-4xl font-normal text-neutral-600 dark:text-neutral-400">
      Vuejs: The
      <FlipWords
        :words="['Progressive', 'Approachable', 'Performant', 'Versatile']"
        :duration="3000"
        class="text-4xl !text-primary"
      />
      <div>
        JavaScript Framework for building <br />
        Modern Web Apps
      </div>
    </div>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="flip-words" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div class="relative inline-block px-2">
    <Transition
      @after-enter="$emit('animationStart')"
      @after-leave="$emit('animationComplete')"
    >
      <div
        v-show="isVisible"
        :class="[
          'relative z-10 inline-block text-left text-neutral-900 dark:text-neutral-100',
          props.class,
        ]"
      >
        <template
          v-for="(wordObj, wordIndex) in splitWords"
          :key="wordObj.word + wordIndex"
        >
          <span
            class="inline-block whitespace-nowrap opacity-0"
            :style="{
              animation: `fadeInWord 0.3s ease forwards`,
              animationDelay: `${wordIndex * 0.3}s`,
            }"
          >
            <span
              v-for="(letter, letterIndex) in wordObj.letters"
              :key="wordObj.word + letterIndex"
              class="inline-block opacity-0"
              :style="{
                animation: `fadeInLetter 0.2s ease forwards`,
                animationDelay: `${wordIndex * 0.3 + letterIndex * 0.05}s`,
              }"
            >
              {{ letter }}
            </span>
            <span class="inline-block">&nbsp;</span>
          </span>
        </template>
      </div>
    </Transition>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";

interface Props {
  words: string[];
  duration?: number;
  class?: string;
}

const props = withDefaults(defineProps<Props>(), {
  duration: 3000,
  class: "",
});

defineEmits(["animationStart", "animationComplete"]);

const currentWord = ref(props.words[0]);
const isVisible = ref(true);
const timeoutId = ref<number | null>(null);

function startAnimation() {
  isVisible.value = false;

  setTimeout(() => {
    const currentIndex = props.words.indexOf(currentWord.value);
    const nextWord = props.words[currentIndex + 1] || props.words[0];
    currentWord.value = nextWord;
    isVisible.value = true;
  }, 600);
}

const splitWords = computed(() => {
  return currentWord.value.split(" ").map((word) => ({
    word,
    letters: word.split(""),
  }));
});

function startTimeout() {
  timeoutId.value = window.setTimeout(() => {
    startAnimation();
  }, props.duration);
}

onMounted(() => {
  startTimeout();
});

onBeforeUnmount(() => {
  if (timeoutId.value) {
    clearTimeout(timeoutId.value);
  }
});

watch(isVisible, (newValue) => {
  if (newValue) {
    startTimeout();
  }
});
</script>

<style>
@keyframes fadeInWord {
  0% {
    opacity: 0;
    transform: translateY(10px);
    filter: blur(8px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}

@keyframes fadeInLetter {
  0% {
    opacity: 0;
    transform: translateY(10px);
    filter: blur(8px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}

.v-enter-active {
  animation: enterWord 0.6s ease-in-out forwards;
}

.v-leave-active {
  animation: leaveWord 0.6s ease-in-out forwards;
}

@keyframes enterWord {
  0% {
    opacity: 0;
    transform: translateY(10px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes leaveWord {
  0% {
    opacity: 1;
    transform: scale(1);
    filter: blur(0);
  }
  100% {
    opacity: 0;
    transform: scale(2);
    filter: blur(8px);
  }
}
</style>

```

## API

| Prop Name  | Type     | Description                                                                                |
| ---------- | -------- | ------------------------------------------------------------------------------------------ |
| `words`    | `Array`  | An array of words to be displayed and animated.                                            |
| `duration` | `number` | Duration (in milliseconds) for each word to be displayed before flipping to the next word. |
| `class`    | `string` | Additional CSS classes to apply to the component.                                          |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for porting this component.

- Ported from [Aceternity UI's Flip Words](https://ui.aceternity.com/components/flip-words)

URL: https://inspira-ui.com/components/text-animations/hyper-text

---
title: Hyper Text
description: A hyper changing text animation as you hover..
---

```vue
<template>
  <div class="flex flex-col items-center justify-center space-y-4">
    <HyperText
      text="Hyper Text"
      class="text-4xl font-bold"
      :duration="800"
      :animate-on-load="true"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="hyper-text" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    :class="cn('flex scale-100 cursor-default overflow-hidden py-2', $props.class)"
    @mouseenter="triggerAnimation"
  >
    <div class="flex">
      <Motion
        v-for="(letter, i) in displayText"
        :key="i"
        as="span"
        :class="cn(letter === ' ' ? 'w-3' : '', $props.class)"
        class="inline-block font-mono"
        :initial="{ opacity: 0, y: -10 }"
        :animate="{ opacity: 1, y: 0 }"
        :delay="i * (duration / (text.length * 10))"
      >
        {{ letter.toUpperCase() }}
      </Motion>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, type HTMLAttributes } from "vue";
import { useIntervalFn } from "@vueuse/core";
import { cn } from "@/lib/utils";
import { Motion } from "motion-v";

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"];
    text: string;
    duration: number;
    animateOnLoad: boolean;
  }>(),
  {
    duration: 800,
  },
);

const alphabets = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const displayText = ref(props.text.split(""));
const iterations = ref(0);

function getRandomLetter() {
  return alphabets[Math.floor(Math.random() * alphabets.length)];
}
function triggerAnimation() {
  iterations.value = 0;
  startAnimation();
}

const { pause, resume } = useIntervalFn(
  () => {
    if (iterations.value < props.text.length) {
      displayText.value = displayText.value.map((l, i) =>
        l === " " ? l : i <= iterations.value ? props.text[i] : getRandomLetter(),
      );
      iterations.value += 0.1;
    } else {
      pause();
    }
  },
  computed(() => props.duration / (props.text.length * 10)),
);

function startAnimation() {
  pause();
  resume();
}

watch(
  () => props.text,
  (newText) => {
    displayText.value = newText.split("");
    triggerAnimation();
  },
);

if (props.animateOnLoad) {
  triggerAnimation();
}
</script>

```

## API

| Prop Name       | Type      | Default  | Description                                               |
| --------------- | --------- | -------- | --------------------------------------------------------- |
| `class`         | `string`  | `""`     | Additional CSS classes to apply to the component.         |
| `text`          | `string`  | Required | Text to animate                                           |
| `duration`      | `number`  | `0.8`    | The total duration (in seconds) for the entire animation. |
| `animateOnLoad` | `boolean` | `true`   | Play animation on load                                    |

## Credits

- Inspired by [Magic UI's Hyper Text](https://magicui.design/docs/components/hyper-text) component.
- Credits to [Prem](https://github.com/premdasvm) for porting this component.

URL: https://inspira-ui.com/components/text-animations/letter-pullup

---
title: Letter Pullup
description: Staggered letter pull up text animation.
---

```vue
<template>
  <LetterPullup
    words="Staggered Letter Pull Up"
    :delay="0.05"
    class="text-black dark:text-white"
  />
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="letter-pullup" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div class="flex justify-center">
    <div
      v-for="(letter, index) in letters"
      :key="letter"
    >
      <Motion
        as="h1"
        :initial="pullupVariant.initial"
        :animate="pullupVariant.animate"
        :transition="{
          delay: index * (props.delay ? props.delay : 0.05),
        }"
        :class="
          cn(
            'font-display text-center text-4xl font-bold tracking-[-0.02em] text-black drop-shadow-sm md:text-4xl md:leading-[5rem]',
            props.class,
          )
        "
      >
        <span v-if="letter === ' '">&nbsp;</span>
        <span v-else>{{ letter }}</span>
      </Motion>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Motion } from "motion-v";
import { cn } from "@/lib/utils";

interface LetterPullupProps {
  class?: string;
  words: string;
  delay?: number;
}

const props = defineProps<LetterPullupProps>();

const letters = props.words.split("");

const pullupVariant = {
  initial: { y: 100, opacity: 0 },
  animate: {
    y: 0,
    opacity: 1,
  },
};
</script>

```

## API

| Prop Name | Type     | Default                    | Description                                        |
| --------- | -------- | -------------------------- | -------------------------------------------------- |
| `class`   | `string` | `-`                        | The class to be applied to the component.          |
| `words`   | `string` | `Staggered Letter Pull Up` | Text to animate.                                   |
| `delay`   | `number` | `0.05`                     | Delay each letter's animation by this many seconds |

## Credits

- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for this component.
- Inspired from [Magic UI](https://magicui.design/docs/components/letter-pullup).

URL: https://inspira-ui.com/components/text-animations/line-shadow-text

---
title: Line Shadow Text
description: A line shadow text component for Magic UI that adds a shadow effect to the text, making it visually appealing and engaging.
---

```vue
<template>
  <div class="flex h-56 w-full flex-col items-center justify-center gap-2">
    <h1 class="text-balance text-8xl font-extrabold leading-none tracking-tighter">
      Ship
      <LineShadowText
        class="italic"
        :shadow-color="shadowColor"
      >
        Fast
      </LineShadowText>
    </h1>
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { useColorMode } from "@vueuse/core";

const isDark = computed(() => useColorMode().value == "dark");
const shadowColor = computed(() => (isDark.value ? "white" : "black"));
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="line-shadow-text" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <component
    :is="as"
    class="shadow-color"
    :class="
      cn(
        'relative z-0 inline-flex',
        'after:absolute after:left-[0.04em] after:top-[0.04em] after:-z-10',
        'after:bg-[linear-gradient(45deg,transparent_45%,var(--shadow-color)_45%,var(--shadow-color)_55%,transparent_0)]',
        'after:bg-[length:0.06em_0.06em] after:bg-clip-text after:text-transparent',
        'after:content-[attr(data-text)]',
        'animate-line-shadow',
        props.class,
      )
    "
    :data-text="content"
  >
    <slot />
  </component>
</template>

<script setup lang="ts">
import { cn } from "@/lib/utils";
import { useSlots, type VNode } from "vue";

interface LineShadowTextProps {
  shadowColor?: string;
  as?: keyof HTMLElement;
  class?: string;
}

const props = withDefaults(defineProps<LineShadowTextProps>(), {
  shadowColor: "black",
  as: "span" as keyof HTMLElement,
});

const slots = useSlots() as Record<string, () => VNode[]>;
const children = slots?.default ? slots.default()[0]?.children : null;

const content = typeof children === "string" ? children : null;

if (!content) {
  throw new Error("LineShadowText only accepts string content");
}
</script>

<style scoped>
.shadow-color {
  --shadow-color: v-bind(props.shadowColor);
}

.animate-line-shadow::after {
  animation: line-shadow 15s linear infinite;
}

@keyframes line-shadow {
  0% {
    background-position: 0 0;
  }
  100% {
    background-position: 100% -100%;
  }
}
</style>

```

## API

| Prop Name     | Type     | Default   | Description                                |
| ------------- | -------- | --------- | ------------------------------------------ |
| `shadowColor` | `string` | `"black"` | The color of the shadow effect             |
| `class`       | `string` | `""`      | Additional CSS classes for custom styling. |
| `as`          | `string` | `"span"`  | The HTML element to render the text as.    |

## Features

- **Slot-Based Content**: Supports default slots for dynamic content, making it flexible for various use cases.

## Credits

- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for this component.
- Ported from [Magic UI's Line Shadow Text](https://magicui.design/docs/components/line-shadow-text)

URL: https://inspira-ui.com/components/text-animations/morphing-text

---
title: Morphing Text
description: This MorphingText component dynamically transitions between an array of text strings, creating a smooth, engaging visual effect.
---

```vue
<template>
  <ClientOnly>
    <MorphingText :texts="texts" />
  </ClientOnly>
</template>

<script lang="ts" setup>
const texts = [
  "Hello",
  "Morphing",
  "Text",
  "Animation",
  "Vue",
  "Component",
  "Smooth",
  "Transition",
  "Engaging",
];
</script>

<style></style>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="morphing-text" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    :class="
      cn(
        'relative mx-auto h-16 w-full max-w-screen-md text-center font-sans text-[40pt] font-bold leading-none [filter:url(#threshold)_blur(0.6px)] md:h-24 lg:text-[6rem]',
        props.class,
      )
    "
  >
    <span
      ref="text1Ref"
      :class="cn(TEXT_CLASSES)"
    />
    <span
      ref="text2Ref"
      :class="cn(TEXT_CLASSES)"
    />

    <svg
      id="filters"
      class="fixed size-0"
      preserveAspectRatio="xMidYMid slice"
    >
      <defs>
        <filter id="threshold">
          <feColorMatrix
            in="SourceGraphic"
            type="matrix"
            values="1 0 0 0 0
                  0 1 0 0 0
                  0 0 1 0 0
                  0 0 0 255 -140"
          />
        </filter>
      </defs>
    </svg>
  </div>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { ref, onMounted, onUnmounted } from "vue";

const TEXT_CLASSES = "absolute inset-x-0 top-0 m-auto inline-block w-full";

interface Props {
  class?: string;
  texts: string[];
  morphTime?: number;
  coolDownTime?: number;
}
const props = withDefaults(defineProps<Props>(), {
  morphTime: 1.5,
  coolDownTime: 0.5,
});

const textIndex = ref(0);
const morph = ref(0);
const coolDown = ref(0);
const time = ref(new Date());

const text1Ref = ref<HTMLSpanElement>();
const text2Ref = ref<HTMLSpanElement>();

function setStyles(fraction: number) {
  if (!text1Ref.value || !text2Ref.value) return;

  text2Ref.value.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`;
  text2Ref.value.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`;

  const invertedFraction = 1 - fraction;
  text1Ref.value.style.filter = `blur(${Math.min(8 / invertedFraction - 8, 100)}px)`;
  text1Ref.value.style.opacity = `${Math.pow(invertedFraction, 0.4) * 100}%`;

  text1Ref.value.textContent = props.texts[textIndex.value % props.texts.length];
  text2Ref.value.textContent = props.texts[(textIndex.value + 1) % props.texts.length];
}

function doMorph() {
  morph.value -= coolDown.value;
  coolDown.value = 0;

  let fraction = morph.value / props.morphTime;

  if (fraction > 1) {
    coolDown.value = props.coolDownTime;
    fraction = 1;
  }

  setStyles(fraction);

  if (fraction === 1) {
    textIndex.value++;
  }
}

function doCoolDown() {
  morph.value = 0;

  if (text1Ref.value && text2Ref.value) {
    text2Ref.value.style.filter = "none";
    text2Ref.value.style.opacity = "100%";
    text1Ref.value.style.filter = "none";
    text1Ref.value.style.opacity = "0%";
  }
}

let animationFrameId: number = 0;
function animate() {
  animationFrameId = requestAnimationFrame(animate);

  const newTime = new Date();
  const dt = (newTime.getTime() - time.value.getTime()) / 1000;
  time.value = newTime;

  coolDown.value -= dt;

  if (coolDown.value <= 0) {
    doMorph();
  } else {
    doCoolDown();
  }
}

onMounted(() => {
  animate();
});

onUnmounted(() => {
  cancelAnimationFrame(animationFrameId);
});
</script>

```

## API

| Prop Name      | Type       | Default | Description                           |
| -------------- | ---------- | ------- | ------------------------------------- |
| `texts`        | `string[]` | `[]`    | Array of texts to morph between.      |
| `class`        | `string`   | `""`    | Additional classes for the container. |
| `morphTime`    | `number`   | `1.5`   | Animation execution time.             |
| `coolDownTime` | `number`   | `0.5`   | Animation dwell time.                 |

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Inspired and ported from [Magic UI Morphing Text](https://magicui.design/docs/components/morphing-text).

URL: https://inspira-ui.com/components/text-animations/number-ticker

---
title: Number Ticker
description: Animate numbers to count up or down to a target number
---

```vue
<template>
  <div class="flex min-h-64 items-center justify-center">
    <p class="whitespace-pre-wrap text-8xl font-medium tracking-tighter text-black dark:text-white">
      <NumberTicker :value="100" />
    </p>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="number-ticker" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <span
    ref="spanRef"
    :class="cn('inline-block tabular-nums text-black dark:text-white tracking-wider', props.class)"
  >
    {{ output }}
  </span>
</template>

<script setup lang="ts">
import { TransitionPresets, useElementVisibility, useTransition } from "@vueuse/core";
import { cn } from "@/lib/utils";
import { ref, watch, computed } from "vue";

type TransitionsPresetsKeys = keyof typeof TransitionPresets;

interface NumberTickerProps {
  value: number;
  direction?: "up" | "down";
  duration?: number;
  delay?: number;
  decimalPlaces?: number;
  class?: string;
  transition?: TransitionsPresetsKeys;
}

const spanRef = ref<HTMLSpanElement>();

const props = withDefaults(defineProps<NumberTickerProps>(), {
  value: 0,
  direction: "up",
  delay: 0,
  duration: 1000,
  decimalPlaces: 2,
  transition: "easeOutCubic",
});

const transitionValue = ref(props.direction === "down" ? props.value : 0);

const transitionOutput = useTransition(transitionValue, {
  delay: props.delay,
  duration: props.duration,
  transition: TransitionPresets[props.transition],
});

const output = computed(() => {
  return new Intl.NumberFormat("en-US", {
    minimumFractionDigits: props.decimalPlaces,
    maximumFractionDigits: props.decimalPlaces,
  }).format(Number(transitionOutput.value.toFixed(props.decimalPlaces)));
});

const isInView = useElementVisibility(spanRef, {
  threshold: 0,
});

watch(
  isInView,
  (isVisible) => {
    if (isVisible) {
      transitionValue.value = props.direction === "down" ? 0 : props.value;
    }
  },
  { immediate: true },
);
</script>

```

---

## API

| Prop Name       | Type                | Default        | Description                                                       |
| --------------- | ------------------- | -------------- | ----------------------------------------------------------------- |
| `value`         | `int`               | `0`            | Value to count to                                                 |
| `direction`     | `up \| down`        | `up`           | Direction to count in                                             |
| `decimalPlaces` | `number`            | `0`            | Number of decimal places to show                                  |
| `delay`         | `number`            | `0`            | Delay before counting (in milliseconds)                           |
| `duration`      | `number`            | `1000`         | Total duration for the entire animation (in milliseconds).        |
| `transition`    | `TransitionPresets` | `easeOutCubic` | Name of transition preset (https://vueuse.org/core/useTransition) |

## Credits

- Credits to [Grzegorz Krol](https://github.com/Grzechu335) for porting this component.
- Ported from [Magic UI NumberTicker](https://magicui.design/docs/components/number-ticker).

URL: https://inspira-ui.com/components/text-animations/radiant-text

---
title: Radiant Text
description: A glare effect on text.
---

```vue
<template>
  <RadiantText
    class="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-600 hover:duration-300 hover:dark:text-neutral-400"
    :duration="5"
  >
    <span class="text-3xl font-bold">✨ Introducing Inspira UI</span>
  </RadiantText>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="radiant-text" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <p
    :style="styleVar"
    :class="
      cn(
        'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',
        // Radiant effect
        'radiant-animation bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--radiant-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
        // Radiant gradient
        'bg-gradient-to-r from-transparent via-black via-50% to-transparent  dark:via-white',
        $props.class,
      )
    "
  >
    <slot />
  </p>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { computed } from "vue";

const props = defineProps({
  duration: {
    type: Number,
    default: 10,
  },
  radiantWidth: {
    type: Number,
    default: 100,
  },
  class: String,
});

const styleVar = computed(() => {
  return {
    "--radiant-anim-duration": `${props.duration}s`,
    "--radiant-width": `${props.radiantWidth}px`,
  };
});
</script>

<style scoped>
@keyframes radiant {
  0%,
  90%,
  100% {
    background-position: calc(-100% - var(--radiant-width)) 0;
  }
  30%,
  60% {
    background-position: calc(100% + var(--radiant-width)) 0;
  }
}

.radiant-animation {
  animation: radiant var(--radiant-anim-duration) infinite;
}
</style>

```

## API

| Prop Name      | Type     | Default | Description                           |
| -------------- | -------- | ------- | ------------------------------------- |
| `duration`     | `number` | `10`    | Duration of the animation in seconds. |
| `radiantWidth` | `number` | `100`   | Width of the radiant animation.       |

## Credits

- Credits to [Magic UI](https://magicui.design/docs/components/animated-shiny-text) for this fantastic component.

URL: https://inspira-ui.com/components/text-animations/sparkles-text

---
title: Sparkles Text
description: A dynamic text that generates continuous sparkles with smooth transitions, perfect for highlighting text with animated stars.
---

```vue
<template>
  <div class="flex h-80 items-center justify-center">
    <SparklesText
      text="Inspira UI"
      :colors="{ first: '#9E7AFF', second: '#FE8BBB' }"
      :sparkles-count="10"
      class="my-8"
    />
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="sparkles-text" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div
    class="text-6xl font-bold"
    :class="props.class"
  >
    <span class="relative inline-block">
      <template
        v-for="sparkle in sparkles"
        :key="sparkle.id"
      >
        <!-- Animated star SVG with fade, scale, and rotation effects -->
        <Motion
          :initial="{ opacity: 0, scale: 0, rotate: 75 }"
          :animate="{
            opacity: [0, 1, 0],
            scale: [0, sparkle.scale, 0],
            rotate: [75, 120, 150],
          }"
          :transition="{
            duration: 0.8,
            repeat: Infinity,
            delay: sparkle.delay,
          }"
          as="svg"
          class="pointer-events-none absolute z-20"
          :style="{
            left: sparkle.x,
            top: sparkle.y,
            opacity: 0,
          }"
          width="21"
          height="21"
          viewBox="0 0 21 21"
        >
          <path
            d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
            :fill="sparkle.color"
          />
        </Motion>
      </template>
      {{ text }}
    </span>
  </div>
</template>

<script setup lang="ts">
import { Motion } from "motion-v";
import { ref, onMounted, onUnmounted } from "vue";

interface Sparkle {
  id: string;
  x: string;
  y: string;
  color: string;
  delay: number;
  scale: number;
  lifespan: number;
}

interface Props {
  text: string;
  sparklesCount?: number;
  colors?: {
    first: string;
    second: string;
  };
  class?: string;
}

const props = withDefaults(defineProps<Props>(), {
  sparklesCount: 10,
  colors: () => ({ first: "#9E7AFF", second: "#FE8BBB" }),
});

const sparkles = ref<Sparkle[]>([]);

// Generate a new sparkle with randomized properties
function generateStar(): Sparkle {
  const starX = `${Math.random() * 100}%`;
  const starY = `${Math.random() * 100}%`;
  const color = Math.random() > 0.5 ? props.colors.first : props.colors.second;
  const delay = Math.random() * 2;
  const scale = Math.random() * 1 + 0.3;
  const lifespan = Math.random() * 10 + 5;
  const id = `${starX}-${starY}-${Date.now()}`;
  return { id, x: starX, y: starY, color, delay, scale, lifespan };
}

// Initialize sparkles array with random stars
function initializeStars() {
  sparkles.value = Array.from({ length: props.sparklesCount }, generateStar);
}

// Update sparkles - regenerate dead ones and update lifespans
function updateStars() {
  sparkles.value = sparkles.value.map((star) => {
    if (star.lifespan <= 0) {
      return generateStar();
    } else {
      return { ...star, lifespan: star.lifespan - 0.1 };
    }
  });
}

let interval: number;

// Start animation loop
onMounted(() => {
  initializeStars();
  interval = window.setInterval(updateStars, 100);
});

// Cleanup on unmount
onUnmounted(() => {
  if (interval) {
    clearInterval(interval);
  }
});
</script>

```

## API

| Prop Name       | Type     | Default                                  | Description                                   |
| --------------- | -------- | ---------------------------------------- | --------------------------------------------- |
| `class`         | `string` | `-`                                      | The class to be applied to the sparkles text. |
| `text`          | `string` | ``                                       | The text to display.                          |
| `sparklesCount` | `number` | `10`                                     | sparkles count that appears on the text.      |
| `colors`        | `object` | `{first: '#A07CFE'; second: '#FE8FB5';}` | The sparkles colors.                          |

## Credits

- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for this component.
- Inspired from [Magic UI](https://magicui.design/docs/components/sparkles-text).
- Credits to [M Atif](https://github.com/atif0075) for updating this component.

URL: https://inspira-ui.com/components/text-animations/spinning-text

---
title: Spinning Text
description: The Spinning Text component animates text in a circular motion with customizable speed, direction, color, and transitions for dynamic and engaging effects.
navBadges:
  - value: New
    type: lime
---

```vue
<template>
  <ClientOnly>
    <div class="flex min-h-[350px] w-full items-center justify-center">
      <SpinningText text="learn more · earn more · grow more ·" />
    </div>
  </ClientOnly>
</template>

<script lang="ts" setup></script>
<style scoped></style>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Examples

reverse

```vue
<template>
  <ClientOnly>
    <div class="flex min-h-[350px] w-full items-center justify-center">
      <SpinningText
        text="learn more · earn more · grow more ·"
        :duration="20"
        reverse
      />
    </div>
  </ClientOnly>
</template>

<script lang="ts" setup></script>
<style scoped></style>

```

## API

| Prop Name    | Type                                                    | Default | Description                                             |
| ------------ | ------------------------------------------------------- | ------- | ------------------------------------------------------- |
| `duration`   | `number`                                                | `10`    | The duration of the full circular rotation animation.   |
| `reverse`    | `boolean`                                               | `false` | Determines if the animation should rotate in reverse.   |
| `radius`     | `number`                                                | `5`     | The radius of the circular path for the text animation. |
| `transition` | `motion-v Transition`                                   | ``      | Custom transition effects for the animation.            |
| `variants`   | `{container: motion-v Variant, item: motion-v Variant}` | ``      | Variants for container and item animations.             |
| `class`      | `string`                                                | `""`    | A custom class name for the text container.             |

## Install using CLI

```vue
<InstallationCli component-id="spinning-text" />
```

## Component Code

You can copy and paste the following code to create this component:

```vue
<template>
  <Motion
    as="div"
    :class="cn('relative', props.class)"
    initial="hidden"
    animate="visible"
    :variants="containerVariants"
    :transition="finalTransition"
  >
    <span
      v-for="(letter, index) in letters"
      :key="`${letter}-${index}`"
      class="absolute left-1/2 top-1/2"
      :variants="itemVariants"
      :style="{
        '--index': index,
        '--total': letters.length,
        '--radius': radius,
        transform: `
                  translate(-50%, -50%)
                  rotate(calc(360deg / var(--total) * var(--index)))
                  translateY(calc(var(--radius, 5) * -1ch))
                `,
        transformOrigin: 'center',
      }"
    >
      {{ letter }}
    </span>
  </Motion>
</template>

<script lang="ts" setup>
import { cn } from "~/lib/utils";
import { Motion } from "motion-v";
import type { Variant, Transition } from "motion-v";

const BASE_TRANSITION = {
  repeat: Infinity,
  ease: "linear",
};

const BASE_ITEM_VARIANTS = {
  hidden: { opacity: 1 },
  visible: { opacity: 1 },
};

interface CircularTextProps {
  text: string;
  duration?: number;
  class?: string;
  reverse?: boolean;
  radius?: number;
  transition?: Transition;
  variants?: {
    container?: Variant;
    item?: Variant;
  };
}

const props = withDefaults(defineProps<CircularTextProps>(), {
  duration: 10,
  radius: 5,
});

const letters = computed(() => {
  let letters = props.text.split("");
  letters.push(" ");
  return letters;
});
const finalTransition = computed(() => ({
  ...BASE_TRANSITION,
  ...props.transition,
  duration: props.transition?.duration ?? props.duration,
}));

const containerVariants = computed(() => ({
  visible: { rotate: props.reverse ? -360 : 360 },
  // ...props.variants?.container,
}));

const itemVariants = computed(() => ({
  ...BASE_ITEM_VARIANTS,
  ...props?.variants?.item,
}));
</script>
<style scoped></style>

```

## Credits

- Credits to [Whbbit1999](https://github.com/Whbbit1999) for this component.
- Ported from [Magic UI Spinning Text](https://magicui.design/docs/components/spinning-text).

URL: https://inspira-ui.com/components/text-animations/text-generate-effect

---
title: Text Generate Effect
description: A cool text effect that fades in text on page load, one by one.
---

```vue
<template>
  <ClientOnly>
    <div class="flex h-64 items-center justify-center max-lg:w-full min-md:flex-1">
      <TextGenerateEffect
        words="Nuxt is an open source framework that makes web development intuitive and powerful.Create performant and production-grade full-stack web apps and websites with confidence."
      />
    </div>
  </ClientOnly>
</template>

<script setup lang="ts"></script>

```

## Install using CLI

```vue
<InstallationCli component-id="text-generate-effect" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <div :class="cn('leading-snug tracking-wide', props.class)">
    <div ref="scope">
      <span
        v-for="(word, idx) in wordsArray"
        :key="word + idx"
        class="inline-block"
        :style="spanStyle"
      >
        {{ word }}&nbsp;
      </span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, type HTMLAttributes, onMounted, ref } from "vue";

import { cn } from "@/lib/utils";

const props = withDefaults(
  defineProps<{
    words: string;
    filter?: boolean;
    duration?: number;
    delay?: number;
    class: HTMLAttributes["class"];
  }>(),
  { duration: 0.7, delay: 0, filter: true },
);

const scope = ref(null);
const wordsArray = computed(() => props.words.split(" "));

const spanStyle = computed(() => ({
  opacity: 0,
  filter: props.filter ? "blur(10px)" : "none",
  transition: `opacity ${props.duration}s, filter ${props.duration}s`,
}));

onMounted(() => {
  if (scope.value) {
    const spans = (scope.value as HTMLElement).querySelectorAll("span");

    setTimeout(() => {
      spans.forEach((span: HTMLElement, index: number) => {
        setTimeout(() => {
          span.style.opacity = "1";
          span.style.filter = props.filter ? "blur(0px)" : "none";
        }, index * 200);
      });
    }, props.delay);
  }
});
</script>

```

## Examples

Two text with different delay

```vue
<template>
  <ClientOnly>
    <div class="flex h-64 flex-col items-center justify-center max-lg:w-full min-md:flex-1">
      <TextGenerateEffect
        words="Nuxt is an open source framework that makes web development intuitive and powerful."
        :delay="0"
      />
      <TextGenerateEffect
        words="Create performant and production-grade full-stack web apps and websites with confidence."
        :delay="3000"
      />
    </div>
  </ClientOnly>
</template>

<script setup lang="ts"></script>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## API

| Prop Name  | Type      | Default  | Description                                                            |
| ---------- | --------- | -------- | ---------------------------------------------------------------------- |
| `words`    | `string`  | Required | The text to be displayed with the generating effect.                   |
| `duration` | `number`  | `0.7`    | The duration of the text generation animation in seconds.              |
| `delay`    | `number`  | `0`      | The delay before the text generation animation starts in milliseconds. |
| `filter`   | `boolean` | `true`   | The blur of the text.                                                  |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for porting this component.

- Ported from [Aceternity UI's Text Generate Effect](https://ui.aceternity.com/components/text-generate-effect).

URL: https://inspira-ui.com/components/text-animations/text-highlight

---
title: Text Highlight
description: A text effect that fill background of a text to highlight it.
---

```vue
<template>
  <div class="text-blance flex justify-center p-10">
    <h1 class="text-balance text-center text-4xl font-bold">
      You are a
      <TextHighlight class="bg-gradient-to-r from-indigo-300 to-purple-300">
        f****ing great developer !
      </TextHighlight>
    </h1>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="text-highlight" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <span :class="cn('inline-block px-1 pb-1', props.class)"><slot /></span>
</template>

<script setup lang="ts">
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

interface Props {
  delay?: number;
  duration?: number;
  class?: HTMLAttributes["class"];
  textEndColor?: string;
}

const props = withDefaults(defineProps<Props>(), {
  delay: 0,
  duration: 2000,
  endColor: "inherit",
});

const delayMs = computed(() => `${props.delay}ms`);
const durationMs = computed(() => `${props.duration}ms`);
</script>

<style scoped>
@keyframes background-expand {
  0% {
    background-size: 0% 100%;
  }
  100% {
    background-size: 100% 100%;
  }
}

@keyframes text-color-change {
  0% {
    color: inherit;
  }
  100% {
    color: v-bind(textEndColor);
  }
}

span {
  background-size: 0% 100%;
  background-repeat: no-repeat;
  background-position: left center;
  animation:
    background-expand v-bind(durationMs) ease-in-out v-bind(delayMs) forwards,
    text-color-change v-bind(durationMs) ease-in-out v-bind(delayMs) forwards;
}
</style>

```

## Examples

With 3s delay

```vue
<template>
  <div class="text-blance flex justify-center p-10">
    <h1 class="text-4xl font-bold">
      Being late for highlights
      <TextHighlight
        :delay="3000"
        class="bg-gradient-to-r from-green-300 to-blue-300"
      >
        is bad ...
      </TextHighlight>
    </h1>
  </div>
</template>

<script setup lang="ts"></script>

```

Rounded text background

```vue
<template>
  <div class="text-blance flex justify-center p-10">
    <h1 class="text-balance text-center text-4xl font-bold">
      Smooth and rounded angles are
      <TextHighlight class="rounded-lg bg-gradient-to-r from-purple-300 to-orange-300">
        the best </TextHighlight
      >. Let's debate.
    </h1>
  </div>
</template>

```

Color from CSS variables: use the paintbrush icon in the top right corner to change the color.

```vue
<template>
  <div class="text-blance flex justify-center p-10">
    <h1 class="text-balance text-center text-4xl font-bold leading-relaxed">
      CSS variables are amazing to match
      <TextHighlight class="my-amazing-class">your design system</TextHighlight>
    </h1>
  </div>
</template>

<style scoped>
.my-amazing-class {
  background-image: linear-gradient(45deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 100%);
}
</style>

```

With text color change.

```vue
<template>
  <div class="text-blance flex justify-center p-10">
    <h1 class="text-4xl font-bold">
      Design should always match
      <TextHighlight
        class="rounded-xl bg-gradient-to-r from-green-300 to-blue-300"
        text-end-color="hsl(var(--accent))"
      >
        WCAG recommendations
      </TextHighlight>
      . Check your contrast.
    </h1>
  </div>
</template>

```

## API

| Prop Name        | Type     | Default   | Description                                                                |
| ---------------- | -------- | --------- | -------------------------------------------------------------------------- |
| `delay`          | `number` | `0`       | Delay before the animation starts, in `ms`.                                |
| `duration`       | `number` | `2000`    | Duration of the animation, in `ms`.                                        |
| `text-end-color` | `string` | `inherit` | Color of the text at the end of the animation. Match WCAG recommendations. |

## Credits

- Inspired by [Aceternity UI](https://ui.aceternity.com/components/hero-highlight)
- Credits to [Nathan De Pachtere](https://nathandepachtere.com) for porting this component.

URL: https://inspira-ui.com/components/text-animations/text-hover-effect

---
title: Text Hover Effect
description: A text hover effect that animates and outlines gradient on hover, as seen on x.ai
---

```vue
<template>
  <ClientOnly>
    <div class="flex h-auto items-center justify-center max-lg:w-full min-md:flex-1">
      <TextHoverEffect
        class="w-[90%] min-lg:min-h-64"
        text="INSPIRA"
      />
    </div>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="text-hover-effect" />
```

## Install Manually

Copy and paste the following code

```vue
<template>
  <svg
    ref="svgRef"
    width="100%"
    height="100%"
    viewBox="0 0 300 100"
    xmlns="http://www.w3.org/2000/svg"
    class="select-none"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
    @mousemove="handleMouseMove"
    @touchstart="handleTouchStart"
    @touchmove="handleTouchMove"
    @touchend="handleTouchEnd"
  >
    <defs>
      <linearGradient
        id="textGradient"
        gradientUnits="userSpaceOnUse"
        cx="50%"
        cy="50%"
        r="25%"
      >
        <stop
          v-if="hovered"
          offset="0%"
          stop-color="var(--yellow-500)"
        />
        <stop
          v-if="hovered"
          offset="25%"
          stop-color="var(--red-500)"
        />
        <stop
          v-if="hovered"
          offset="50%"
          stop-color="var(--blue-500)"
        />
        <stop
          v-if="hovered"
          offset="75%"
          stop-color="var(--cyan-500)"
        />
        <stop
          v-if="hovered"
          offset="100%"
          stop-color="var(--violet-500)"
        />
      </linearGradient>

      <!-- Radial Gradient -->
      <radialGradient
        id="revealMask"
        gradientUnits="userSpaceOnUse"
        r="20%"
        :cx="maskPosition.cx"
        :cy="maskPosition.cy"
        :style="{
          transition: `cx ${transitionDuration}ms ease-out, cy ${transitionDuration}ms ease-out`,
        }"
      >
        <stop
          offset="0%"
          stop-color="white"
        />
        <stop
          offset="100%"
          stop-color="black"
        />
      </radialGradient>

      <mask id="textMask">
        <rect
          x="0"
          y="0"
          width="100%"
          height="100%"
          fill="url(#revealMask)"
        />
      </mask>
    </defs>

    <text
      x="50%"
      y="50%"
      text-anchor="middle"
      dominant-baseline="middle"
      :stroke-width="strokeWidth"
      :style="{ opacity: hovered ? opacity : 0 }"
      class="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
    >
      {{ text }}
    </text>

    <!-- Animated Text Stroke -->
    <text
      x="50%"
      y="50%"
      text-anchor="middle"
      dominant-baseline="middle"
      :stroke-width="strokeWidth"
      :style="strokeStyle"
      class="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
    >
      {{ text }}
    </text>

    <text
      x="50%"
      y="50%"
      text-anchor="middle"
      dominant-baseline="middle"
      stroke="url(#textGradient)"
      :stroke-width="strokeWidth"
      mask="url(#textMask)"
      class="fill-transparent font-[helvetica] text-7xl font-bold"
    >
      {{ text }}
    </text>
  </svg>
</template>

<script setup lang="ts">
import { ref, reactive, computed } from "vue";

interface Props {
  strokeWidth?: number;
  text: string;
  duration?: number;
  opacity?: number;
}

const props = withDefaults(defineProps<Props>(), {
  strokeWidth: 0.75,
  duration: 200,
  opacity: 0.75,
});

const svgRef = ref<SVGSVGElement | null>(null);
const cursor = reactive({ x: 0, y: 0 });
const hovered = ref(false);

// Set transition duration for smoother animation
const transitionDuration = props.duration ? props.duration * 1000 : 200;

// Reactive gradient position
const maskPosition = computed(() => {
  if (svgRef.value) {
    const svgRect = svgRef.value.getBoundingClientRect();
    const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
    const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
    return { cx: `${cxPercentage}%`, cy: `${cyPercentage}%` };
  }
  return { cx: "50%", cy: "50%" }; // Default position
});

// Reactive style for stroke animation
const strokeStyle = computed(() => ({
  strokeDashoffset: hovered.value ? "0" : "1000",
  strokeDasharray: "1000",
  transition: "stroke-dashoffset 4s ease-in-out, stroke-dasharray 4s ease-in-out",
}));

function handleMouseEnter() {
  hovered.value = true;
}

function handleMouseLeave() {
  hovered.value = false;
}

function handleMouseMove(e: MouseEvent) {
  cursor.x = e.clientX;
  cursor.y = e.clientY;
}

// Touch support
function handleTouchStart(e: TouchEvent) {
  hovered.value = true;
  handleTouchMove(e); // Update the position on touch start
}

function handleTouchMove(e: TouchEvent) {
  const touch = e.touches[0];
  cursor.x = touch.clientX;
  cursor.y = touch.clientY;
}

function handleTouchEnd() {
  hovered.value = false;
}
</script>

<style scoped>
.select-none {
  user-select: none;
}
</style>

```

## API

| Prop Name     | Type     | Default  | Description                                               |
| ------------- | -------- | -------- | --------------------------------------------------------- |
| `text`        | `string` | Required | The text to be displayed with the hover effect.           |
| `duration`    | `number` | `200`    | The duration of the mask transition animation in seconds. |
| `strokeWidth` | `number` | `0.75`   | The width of the text stroke.                             |
| `opacity`     | `number` | `null`   | The opacity of the text.                                  |

URL: https://inspira-ui.com/components/text-animations/text-reveal-card

---
title: Text Reveal Card
description: Mousemove effect to reveal text content at the bottom of the card.
---

```vue
<template>
  <TextRevealCard class="mx-auto my-8">
    <template #header>
      <h2 class="mb-2 text-lg font-semibold text-white">Text Reveal</h2>
      <p class="text-sm text-[#a9a9a9]">Hover over the text to reveal the animation</p>
    </template>
    <template #text>
      <p
        class="bg-[#323238] bg-clip-text py-4 text-sm font-bold text-transparent md:py-10 md:text-[3rem] sm:py-6 sm:text-3xl"
      >
        Get ready to see what's hidden
      </p>
    </template>
    <template #revealText>
      <p
        :style="{
          textShadow: '4px 4px 15px rgba(0,0,0,0.5)',
        }"
        class="bg-gradient-to-b from-white to-neutral-300 bg-clip-text py-4 text-sm font-bold text-white md:py-10 md:text-[3rem] sm:py-6 sm:text-3xl"
      >
        Light reveals what shadows hide
      </p>
    </template>
  </TextRevealCard>
  <TextRevealCard
    :stars-count="500"
    stars-class="bg-red-500"
    class="mx-auto my-8"
  >
    <template #header>
      <h2 class="mb-2 text-3xl font-semibold">Text Reveal</h2>
      <p class="text-sm text-[#a9a9a9]">Hover over the text to reveal the animation</p>
    </template>
    <template #text>
      <p class="text-white">
        A reveal animation smoothly unveils hidden content, enhancing user interaction. It's
        triggered by scrolling, clicking, or viewport entry, adding dynamic transitions to web
        elements.
      </p>
    </template>
    <template #revealText>
      <p class="text-red-500">
        A ɿɘvɘɒl ɒniɱɒʇiou ƨɱoothly unvɘilƨ ʜiddɘn coɴtɘnt, ɘnhɒncing uƨɘɿ intɘɿɒction. It'ƨ
        tɿiggɘɿɘd by ƨcɿolling, clicʞing, oɿ viɘwpoɿt ɘntɿy, ɒdding dynɒmic tɿɒnƨitionƨ to wɘb
        ɘlɘmɘntƨ.
      </p>
    </template>
  </TextRevealCard>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="text-reveal-card" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="TextRevealCard.vue" language="vue" componentName="TextRevealCard" type="ui" id="text-reveal-card"}
:CodeViewerTab{filename="TextRevealStars.vue" language="vue" componentName="TextRevealStars" type="ui" id="text-reveal-card"}
::

## API

| Prop Name  | Type     | Description                                                      |
| ---------- | -------- | ---------------------------------------------------------------- |
| class      | `String` | Additional classes to be added to the card.                      |
| starsCount | `Number` | Control the number of stars that are generated                   |
| starsClass | `String` | Additional classes to be added to the stars floating on content. |

| Slot Name  | Description                                             |
| ---------- | ------------------------------------------------------- |
| header     | `String`                                                |
| text       | Display default text when the card is not hovered over. |
| revealText | Text to be revealed when hovered over the card.         |

## Credits

- Credits to [M Atif](https://github.com/atif0075) for porting this component.

- Ported from [Aceternity UI's Text Reveal Card](https://ui.aceternity.com/components/text-reveal-card).

URL: https://inspira-ui.com/components/text-animations/text-scroll-reveal

---
title: Text Scroll Reveal
description: A component that reveals text word by word as you scroll, with customizable text and styling.
---

```vue
<template>
  <ClientOnly>
    <div
      class="z-10 flex min-h-64 items-center justify-center rounded-lg border bg-white dark:bg-black"
    >
      <TextScrollReveal text="Making UI beautiful using Inspira UI." />
    </div>
  </ClientOnly>
</template>

```

::alert{type="warning"}
This component uses the `nuxt-only` syntax with the `<ClientOnly>`. If you are not using Nuxt, you can simply remove it.
::

## Install using CLI

```vue
<InstallationCli component-id="text-scroll-reveal" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="TextScrollReveal.vue" language="vue" componentName="TextScrollReveal" type="ui" id="text-scroll-reveal"}
:CodeViewerTab{filename="ScrollWord.vue" language="vue" componentName="ScrollWord" type="ui" id="text-scroll-reveal"}
::

## API

| Prop Name | Type     | Default | Description                                                         |
| --------- | -------- | ------- | ------------------------------------------------------------------- |
| `text`    | `string` | N/A     | The text content to display and reveal word by word during scroll.  |
| `class`   | `string` | `""`    | Additional CSS classes to apply to the component for customization. |

## Features

- **Scroll-Activated Text Reveal**: The component reveals the provided text word by word as the user scrolls, creating an engaging visual effect.

- **Customizable Text Content**: Set the `text` prop to display any text content you wish to reveal during scroll.

- **Smooth Animations**: Each word's opacity transitions smoothly based on scroll position, providing a visually appealing experience.

- **Styling Flexibility**: Use the `class` prop to pass additional CSS classes for custom styling.

- **Reactive Design**: The component uses Vue's reactivity system to update the scroll progress and word visibility in real-time.

## Credits

- Ported from [Magic UI Text Reveal](https://magicui.design/docs/components/text-reveal).

URL: https://inspira-ui.com/components/visualization/carousal-3d

---
title: 3D Carousel
description: A dynamic and interactive 3D carousel component using Three.js and Motion-V, allowing smooth infinite rotation and user-controlled interactions.
---

```vue
<template>
  <Carousel3D :items="items"></Carousel3D>
</template>

<script setup lang="ts">
const items = [
  "https://images.pexels.com/photos/799443/pexels-photo-799443.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
  "https://images.pexels.com/photos/16245254/pexels-photo-16245254/free-photo-of-chatgpt-a-chatbot-for-your-website.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
  "https://images.pexels.com/photos/1910236/pexels-photo-1910236.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
  "https://images.pexels.com/photos/2832382/pexels-photo-2832382.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
  "https://images.pexels.com/photos/2333293/pexels-photo-2333293.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
  "https://images.pexels.com/photos/604684/pexels-photo-604684.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
  "https://images.pexels.com/photos/3308588/pexels-photo-3308588.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
  "https://images.pexels.com/photos/2860807/pexels-photo-2860807.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
];
</script>

```

## API

| Prop Name        | Type        | Default | Description                                                 |
| ---------------- | ----------- | ------- | ----------------------------------------------------------- |
| `items`          | `unknown[]` | `[]`    | List of images or elements to be displayed in the carousel. |
| `class`          | `string`    | `""`    | Additional CSS classes for styling the carousel overlay.    |
| `containerClass` | `string`    | `""`    | CSS classes for styling the carousel container.             |
| `width`          | `number`    | `450`   | Width of individual carousel items.                         |
| `height`         | `number`    | `600`   | Height of individual carousel items.                        |

## Component Code

You can copy and paste the following code to create this component:

::code-group

    ::CodeViewerTab{label="Carousel3D.vue" language="vue" componentName="Carousel3D" type="ui" id="carousel-3d"}
    ::

::

## Features

- **3D Rotating Carousel**: Displays a rotating 3D carousel using Three.js.
- **Smooth Infinite Rotation**: The carousel continuously rotates smoothly.
- **User Interaction Support**: Supports mouse and touch interactions to manually rotate the carousel.
- **Fully Responsive**: Adapts to different screen sizes dynamically.
- **Performance Optimized**: Utilizes Motion-V for smooth animations and easing functions.
- **Dynamic Item Rendering**: Accepts an array of items to render in the carousel dynamically.
- **Dark Mode Support**: Adaptable to dark and light themes for better UI consistency.

## Credits

- Built using Three.js for 3D rendering.
- Utilizes Motion-V for seamless animations.
- Thanks [@safakdinc](https://github.com/safakdinc) for sharing this component.

URL: https://inspira-ui.com/components/visualization/file-tree

---
title: File Tree
description: A component used to showcase the folder and file structure of a directory.
---

```vue
<template>
  <Tree
    class="overflow-hidden rounded-md bg-background p-2"
    :initial-selected-id="'1'"
    :initial-expanded-items="[
      '1',
      '2',
      '3',
      '4',
      '5',
      '6',
      '7',
      '8',
      '9',
      '10',
      '11',
      '12',
      '13',
      '14',
      '15',
      '16',
      '17',
      '18',
      '19',
    ]"
    :elements="ELEMENTS"
  >
    <Folder
      id="1"
      name="root"
    >
      <Folder
        id="2"
        name="components"
      >
        <Folder
          id="3"
          name="ui"
        >
          <File
            id="4"
            name="Button.vue"
          />
          <File
            id="5"
            name="Card.vue"
          />
          <File
            id="6"
            name="Input.vue"
          />
        </Folder>
        <File
          id="7"
          name="Header.vue"
        />
        <File
          id="8"
          name="Footer.vue"
        />
      </Folder>
      <Folder
        id="9"
        name="composables"
      >
        <File
          id="10"
          name="useAuth.ts"
          :is-selectable="false"
        />
        <File
          id="11"
          name="useTheme.ts"
        />
      </Folder>
      <Folder
        id="12"
        name="layouts"
      >
        <File
          id="13"
          name="default.vue"
        />
        <File
          id="14"
          name="auth.vue"
          :is-selectable="false"
        />
      </Folder>
      <Folder
        id="15"
        name="pages"
      >
        <File
          id="16"
          name="index.vue"
        />
        <File
          id="17"
          name="about.vue"
        />
        <Folder
          id="18"
          name="auth"
          :is-selectable="false"
        >
          <File
            id="19"
            name="login.vue"
          />
          <File
            id="20"
            name="register.vue"
          />
        </Folder>
      </Folder>
      <File
        id="21"
        name="app.vue"
      />
      <File
        id="22"
        name="nuxt.config.ts"
      />
    </Folder>
  </Tree>
</template>

<script setup lang="ts">
import type { TreeViewElement } from "~/components/content/inspira/ui/file-tree/index";

const ELEMENTS: TreeViewElement[] = [
  {
    id: "1",
    isSelectable: true,
    name: "root",
    children: [
      {
        id: "2",
        isSelectable: true,
        name: "components",
        children: [
          {
            id: "3",
            isSelectable: true,
            name: "ui",
            children: [
              { id: "4", isSelectable: true, name: "Button.vue" },
              { id: "5", isSelectable: true, name: "Card.vue" },
              { id: "6", isSelectable: true, name: "Input.vue" },
            ],
          },
          { id: "7", isSelectable: true, name: "Header.vue" },
          { id: "8", isSelectable: true, name: "Footer.vue" },
        ],
      },
      {
        id: "9",
        isSelectable: true,
        name: "composables",
        children: [
          { id: "10", isSelectable: false, name: "useAuth.ts" },
          { id: "11", isSelectable: true, name: "useTheme.ts" },
        ],
      },
      {
        id: "12",
        isSelectable: true,
        name: "layouts",
        children: [
          { id: "13", isSelectable: true, name: "default.vue" },
          { id: "14", isSelectable: false, name: "auth.vue" },
        ],
      },
      {
        id: "15",
        isSelectable: true,
        name: "pages",
        children: [
          { id: "16", isSelectable: true, name: "index.vue" },
          { id: "17", isSelectable: true, name: "about.vue" },
          {
            id: "18",
            isSelectable: false,
            name: "auth",
            children: [
              { id: "19", isSelectable: true, name: "login.vue" },
              { id: "20", isSelectable: true, name: "register.vue" },
            ],
          },
        ],
      },
      { id: "21", isSelectable: true, name: "app.vue" },
      { id: "22", isSelectable: true, name: "nuxt.config.ts" },
    ],
  },
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="file-tree" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Tree.vue" language="vue" componentName="Tree" type="ui" id="file-tree"}
:CodeViewerTab{label="Folder.vue" language="vue" componentName="Folder" type="ui" id="file-tree"}
:CodeViewerTab{label="File.vue" language="vue" componentName="File" type="ui" id="file-tree"}
:CodeViewerTab{label="TreeIndicator.vue" language="vue" componentName="TreeIndicator" type="ui" id="file-tree"}

```ts [index.ts]
import type { HTMLAttributes } from "vue";

export interface TreeProps {
  class?: HTMLAttributes["class"];
  initialSelectedId: string;
  indicator?: boolean;
  elements: TreeViewElement[];
  initialExpandedItems: string[];
  openIcon?: string;
  closeIcon?: string;
  fileIcon?: string;
  direction?: "rtl" | "ltr";
}

export interface TreeContextProps {
  selectedId: Ref<string | undefined>;
  expandedItems: Ref<string[] | undefined>;
  indicator: boolean;
  openIcon: string;
  closeIcon: string;
  fileIcon: string;
  direction: "rtl" | "ltr";
  handleExpand: (id: string) => void;
  selectItem: (id: string) => void;
  setExpandedItems: (items: string[] | undefined) => void;
}

export interface TreeViewElement {
  id: string;
  name: string;
  isSelectable?: boolean;
  children?: TreeViewElement[];
}

export interface BaseItemProps {
  class?: HTMLAttributes["class"];
  id: string;
  name: string;
  isSelectable?: boolean;
  isSelect?: boolean;
}

export interface FolderProps extends BaseItemProps {}

export interface FileProps extends BaseItemProps {}

export const TREE_CONTEXT_SYMBOL = Symbol("TREE_CONTEXT_SYMBOL");

export { default as Tree } from "./Tree.vue";
export { default as Folder } from "./Folder.vue";
export { default as File } from "./File.vue";
```

::

## API

::steps

### `Tree`

The `Tree` component serves as a container for displaying a hierarchical file/folder structure.

#### Props

| Prop Name              | Type                | Default                | Description                                        |
| ---------------------- | ------------------- | ---------------------- | -------------------------------------------------- |
| `class`                | `string`            | -                      | Additional classes for styling the tree container. |
| `initialSelectedId`    | `string`            | -                      | ID of the initially selected item.                 |
| `indicator`            | `boolean`           | `true`                 | Whether to show the tree indicator line.           |
| `elements`             | `TreeViewElement[]` | -                      | Array of tree elements to display.                 |
| `initialExpandedItems` | `string[]`          | -                      | Array of IDs of initially expanded folders.        |
| `openIcon`             | `string`            | `"lucide:folder-open"` | Icon to show for open folders.                     |
| `closeIcon`            | `string`            | `"lucide:folder"`      | Icon to show for closed folders.                   |
| `fileIcon`             | `string`            | `"lucide:file"`        | Icon to show for files.                            |
| `direction`            | `"rtl" \| "ltr"`    | `"ltr"`                | Text direction of the tree.                        |

#### Usage

```vue [MyComponent.vue]
<Tree initialSelectedId="1" :initial-expanded-items="['1', '2']" :elements="ELEMENTS">
  <!-- Your structure here -->
</Tree>
```

### `Folder` and `File`

The `Folder` and `File` components represent folders and files in the file tree. Folders can contain other `Folder` and `File` components.

#### Props

| Prop Name      | Type      | Default | Description                             |
| -------------- | --------- | ------- | --------------------------------------- |
| `class`        | `string`  | -       | Additional classes for custom styling.  |
| `id`           | `string`  | -       | Unique identifier for the item.         |
| `name`         | `string`  | -       | Display name of the folder/file.        |
| `isSelectable` | `boolean` | `true`  | Whether the item can be selected.       |
| `isSelect`     | `boolean` | `false` | Whether the item is currently selected. |

#### Usage

```vue [MyComponent.vue]
<Folder id="1" name="root">
  <Folder id="2" name="components">
    <File id="3" name="Button.vue" />
    <File id="4" name="Card.vue" />
  </Folder>
</Folder>
```

::

## Credits

- Inspired by [Magic UI](https://magicui.design/docs/components/file-tree).
- Credit to [kalix127](https://github.com/kalix127) for porting this component.

URL: https://inspira-ui.com/components/visualization/github-globe

---
title: Github Globe
description: A 3D interactive globe visualization with customizable arcs, points, and animation options inspired from Github.
---

```vue
<template>
  <ClientOnly>
    <div class="flex w-full flex-col items-center justify-center">
      <GithubGlobe
        :globe-config="globeConfig"
        :data="sampleArcs"
        class="h-[32rem]"
      />
    </div>
  </ClientOnly>
</template>

<script setup lang="ts">
const globeConfig = {
  pointSize: 1,
  globeColor: "#0b43bd",
  showAtmosphere: true,
  atmosphereColor: "#FFFFFF",
  atmosphereAltitude: 0.1,
  emissive: "#062056",
  emissiveIntensity: 0.1,
  shininess: 0.9,
  polygonColor: "rgba(255,255,255,1)",
  ambientLight: "#38bdf8",
  directionalLeftLight: "#ffffff",
  directionalTopLight: "#ffffff",
  pointLight: "#ffffff",
  arcTime: 1000,
  arcLength: 1,
  rings: 1,
  maxRings: 10,
  initialPosition: { lat: 22.3193, lng: 114.1694 },
  autoRotate: true,
  autoRotateSpeed: 0.5,
};

const colors = [
  "#eae547",
  "#9347ea",
  "#d4ea47",
  "#ddea47",
  "#47ea70",
  "#eab447",
  "#eaa647",
  "#c747ea",
  "#52ea47",
  "#4754ea",
];
const sampleArcs = [
  {
    order: 1,
    startLat: -19.885592,
    startLng: -43.951191,
    endLat: -22.9068,
    endLng: -43.1729,
    arcAlt: 0.1,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 1,
    startLat: 28.6139,
    startLng: 77.209,
    endLat: 3.139,
    endLng: 101.6869,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 1,
    startLat: -19.885592,
    startLng: -43.951191,
    endLat: -1.303396,
    endLng: 36.852443,
    arcAlt: 0.5,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 2,
    startLat: 1.3521,
    startLng: 103.8198,
    endLat: 35.6762,
    endLng: 139.6503,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 2,
    startLat: 51.5072,
    startLng: -0.1276,
    endLat: 3.139,
    endLng: 101.6869,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 2,
    startLat: -15.785493,
    startLng: -47.909029,
    endLat: 36.162809,
    endLng: -115.119411,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 3,
    startLat: -33.8688,
    startLng: 151.2093,
    endLat: 22.3193,
    endLng: 114.1694,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 3,
    startLat: 21.3099,
    startLng: -157.8581,
    endLat: 40.7128,
    endLng: -74.006,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 3,
    startLat: -6.2088,
    startLng: 106.8456,
    endLat: 51.5072,
    endLng: -0.1276,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 4,
    startLat: 11.986597,
    startLng: 8.571831,
    endLat: -15.595412,
    endLng: -56.05918,
    arcAlt: 0.5,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 4,
    startLat: -34.6037,
    startLng: -58.3816,
    endLat: 22.3193,
    endLng: 114.1694,
    arcAlt: 0.7,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 4,
    startLat: 51.5072,
    startLng: -0.1276,
    endLat: 48.8566,
    endLng: -2.3522,
    arcAlt: 0.1,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 5,
    startLat: 14.5995,
    startLng: 120.9842,
    endLat: 51.5072,
    endLng: -0.1276,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 5,
    startLat: 1.3521,
    startLng: 103.8198,
    endLat: -33.8688,
    endLng: 151.2093,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 5,
    startLat: 34.0522,
    startLng: -118.2437,
    endLat: 48.8566,
    endLng: -2.3522,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 6,
    startLat: -15.432563,
    startLng: 28.315853,
    endLat: 1.094136,
    endLng: -63.34546,
    arcAlt: 0.7,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 6,
    startLat: 37.5665,
    startLng: 126.978,
    endLat: 35.6762,
    endLng: 139.6503,
    arcAlt: 0.1,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 6,
    startLat: 22.3193,
    startLng: 114.1694,
    endLat: 51.5072,
    endLng: -0.1276,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 7,
    startLat: -19.885592,
    startLng: -43.951191,
    endLat: -15.595412,
    endLng: -56.05918,
    arcAlt: 0.1,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 7,
    startLat: 48.8566,
    startLng: -2.3522,
    endLat: 52.52,
    endLng: 13.405,
    arcAlt: 0.1,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 7,
    startLat: 52.52,
    startLng: 13.405,
    endLat: 34.0522,
    endLng: -118.2437,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 8,
    startLat: -8.833221,
    startLng: 13.264837,
    endLat: -33.936138,
    endLng: 18.436529,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 8,
    startLat: 49.2827,
    startLng: -123.1207,
    endLat: 52.3676,
    endLng: 4.9041,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 8,
    startLat: 1.3521,
    startLng: 103.8198,
    endLat: 40.7128,
    endLng: -74.006,
    arcAlt: 0.5,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 9,
    startLat: 51.5072,
    startLng: -0.1276,
    endLat: 34.0522,
    endLng: -118.2437,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 9,
    startLat: 22.3193,
    startLng: 114.1694,
    endLat: -22.9068,
    endLng: -43.1729,
    arcAlt: 0.7,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 9,
    startLat: 1.3521,
    startLng: 103.8198,
    endLat: -34.6037,
    endLng: -58.3816,
    arcAlt: 0.5,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 10,
    startLat: -22.9068,
    startLng: -43.1729,
    endLat: 28.6139,
    endLng: 77.209,
    arcAlt: 0.7,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 10,
    startLat: 34.0522,
    startLng: -118.2437,
    endLat: 31.2304,
    endLng: 121.4737,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 10,
    startLat: -6.2088,
    startLng: 106.8456,
    endLat: 52.3676,
    endLng: 4.9041,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 11,
    startLat: 41.9028,
    startLng: 12.4964,
    endLat: 34.0522,
    endLng: -118.2437,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 11,
    startLat: -6.2088,
    startLng: 106.8456,
    endLat: 31.2304,
    endLng: 121.4737,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 11,
    startLat: 22.3193,
    startLng: 114.1694,
    endLat: 1.3521,
    endLng: 103.8198,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 12,
    startLat: 34.0522,
    startLng: -118.2437,
    endLat: 37.7749,
    endLng: -122.4194,
    arcAlt: 0.1,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 12,
    startLat: 35.6762,
    startLng: 139.6503,
    endLat: 22.3193,
    endLng: 114.1694,
    arcAlt: 0.2,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 12,
    startLat: 22.3193,
    startLng: 114.1694,
    endLat: 34.0522,
    endLng: -118.2437,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 13,
    startLat: 52.52,
    startLng: 13.405,
    endLat: 22.3193,
    endLng: 114.1694,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 13,
    startLat: 11.986597,
    startLng: 8.571831,
    endLat: 35.6762,
    endLng: 139.6503,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 13,
    startLat: -22.9068,
    startLng: -43.1729,
    endLat: -34.6037,
    endLng: -58.3816,
    arcAlt: 0.1,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
  {
    order: 14,
    startLat: -33.936138,
    startLng: 18.436529,
    endLat: 21.395643,
    endLng: 39.883798,
    arcAlt: 0.3,
    color: colors[Math.floor(Math.random() * (colors.length - 1))],
  },
];
</script>

```

::alert{type="info"}
**Note:** This component uses Three.js & requires `three`, `three-globe` & `postprocessing` npm package as a dependency.
::

::alert{type="warning"}
**For Nuxt users**:
Use `<ClientOnly>` tag to wrap this component to avoid `window is not defined` error.
::

## Install using CLI

```vue
<InstallationCli component-id="github-globe" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install three postprocessing three-globe
npm install -D @types/three
```

```bash [pnpm]
pnpm install three postprocessing three-globe
pnpm install -D @types/three
```

```bash [bun]
bun add three postprocessing three-globe
bun add -d @types/three
```

```bash [yarn]
yarn add three postprocessing three-globe
yarn add --dev @types/three
```

::

Copy and paste the following code

```vue
<template>
  <canvas
    ref="githubGlobeRef"
    :class="cn('w-96 h-96', props.class)"
  ></canvas>
</template>

<script lang="ts" setup>
// Download globe json file from https://geojson-maps.kyd.au/ and save in the same folder
/* eslint-disable @typescript-eslint/no-explicit-any */

import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import ThreeGlobe from "three-globe";
import {
  AmbientLight,
  Color,
  DirectionalLight,
  PerspectiveCamera,
  PointLight,
  Scene,
  WebGLRenderer,
} from "three";
import contries from "./globe.json";
import { cn } from "@/lib/utils";
import { ref, onMounted, onBeforeUnmount, watch } from "vue";

type Position = {
  order: number;
  startLat: number;
  startLng: number;
  endLat: number;
  endLng: number;
  arcAlt: number;
  color: string;
};

interface GlobeData {
  size: number | undefined;
  order: number;
  color: (t: number) => string;
  lat: number;
  lng: number;
}

interface GlobeConfig {
  pointSize?: number;
  globeColor?: string;
  showAtmosphere?: boolean;
  atmosphereColor?: string;
  atmosphereAltitude?: number;
  emissive?: string;
  emissiveIntensity?: number;
  shininess?: number;
  polygonColor?: string;
  ambientLight?: string;
  directionalLeftLight?: string;
  directionalTopLight?: string;
  pointLight?: string;
  arcTime?: number;
  arcLength?: number;
  rings?: number;
  maxRings?: number;
  initialPosition?: {
    lat: number;
    lng: number;
  };
  autoRotate?: boolean;
  autoRotateSpeed?: number;
}

interface Props {
  globeConfig?: GlobeConfig;
  data?: Position[];
  class?: string;
}

const props = withDefaults(defineProps<Props>(), {
  globeConfig: () => {
    return {};
  },
  data: () => [],
});

const defaultGlobeConfig: GlobeConfig = {
  pointSize: 1,
  atmosphereColor: "#ffffff",
  showAtmosphere: true,
  atmosphereAltitude: 0.1,
  polygonColor: "rgba(255,255,255,0.7)",
  globeColor: "#1d072e",
  emissive: "#000000",
  emissiveIntensity: 0.1,
  shininess: 0.9,
  arcTime: 2000,
  arcLength: 0.9,
  rings: 1,
  maxRings: 3,
  ...props.globeConfig,
};

const githubGlobeRef = ref<HTMLCanvasElement>();
const globeData = ref<GlobeData[]>();

let numberOfRings: number[] = [];

let renderer: WebGLRenderer;
let scene: Scene;
let camera: PerspectiveCamera;
let controls: OrbitControls;

let globe: ThreeGlobe;

onMounted(() => {
  setupScene();
  initGlobe();
  startAnimation();
  animate();

  onWindowResize();

  window.addEventListener("resize", onWindowResize, false);

  watch(globeData, () => {
    if (!globe || !globeData.value) return;

    numberOfRings = genRandomNumbers(0, props.data.length, Math.floor((props.data.length * 4) / 5));

    globe.ringsData(globeData.value.filter((d, i) => numberOfRings.includes(i)));
  });
});

function setupScene() {
  if (!githubGlobeRef.value) {
    throw new Error("Canvas not initialized");
  }

  const width = githubGlobeRef.value.clientWidth;
  const height = githubGlobeRef.value.clientHeight;

  renderer = new WebGLRenderer({ canvas: githubGlobeRef.value, antialias: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setSize(width, height);
  renderer.autoClear = false;

  scene = new Scene();

  camera = new PerspectiveCamera();
  camera.aspect = width / height;
  camera.position.setX(0);
  camera.position.setY(0);
  camera.position.setZ(400);

  const ambientLight = new AmbientLight(defaultGlobeConfig.ambientLight || "#ffffff", 0.6);
  scene.add(ambientLight);

  const dLight1 = new DirectionalLight(defaultGlobeConfig.directionalLeftLight || "#ffffff", 1);
  dLight1.position.set(-400, 100, 400);
  camera.add(dLight1);

  const dLight2 = new DirectionalLight(defaultGlobeConfig.directionalTopLight || "#ffffff", 1);
  dLight2.position.set(-200, 500, 200);
  camera.add(dLight2);

  const pLight = new PointLight(defaultGlobeConfig.pointLight || "#ffffff", 0.8);
  pLight.position.set(-200, 500, 200);
  camera.add(pLight);

  camera.updateProjectionMatrix();
  scene.add(camera);

  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableZoom = false;
  controls.enablePan = false;
  controls.enableDamping = true;
  controls.dampingFactor = 0.01;
  controls.minDistance = 200;
  controls.maxDistance = 500;
  controls.rotateSpeed = defaultGlobeConfig.autoRotateSpeed || 0.8;
  controls.zoomSpeed = 1;
  controls.autoRotate = defaultGlobeConfig.autoRotate || false;

  controls.minPolarAngle = Math.PI / 3.5;
  controls.maxPolarAngle = Math.PI - Math.PI / 3;
}

function initGlobe() {
  buildData();

  globe = new ThreeGlobe({
    waitForGlobeReady: true,
    animateIn: true,
  })
    .hexPolygonsData(contries.features)
    .hexPolygonResolution(3)
    .hexPolygonMargin(0.7)
    .showAtmosphere(defaultGlobeConfig.showAtmosphere!)
    .atmosphereColor(defaultGlobeConfig.atmosphereColor!)
    .atmosphereAltitude(defaultGlobeConfig.atmosphereAltitude!)
    .hexPolygonColor((e) => defaultGlobeConfig.polygonColor!);

  globe.rotateY(-Math.PI * (5 / 9));
  globe.rotateZ(-Math.PI / 6);

  const globeMaterial = globe.globeMaterial() as unknown as {
    color: Color;
    emissive: Color;
    emissiveIntensity: number;
    shininess: number;
  };

  globeMaterial.color = new Color(defaultGlobeConfig.globeColor!);
  globeMaterial.emissive = new Color(defaultGlobeConfig.emissive!);
  globeMaterial.emissiveIntensity = defaultGlobeConfig.emissiveIntensity || 0.1;
  globeMaterial.shininess = defaultGlobeConfig.shininess || 0.9;

  scene.add(globe);
}

function onWindowResize() {
  if (!githubGlobeRef.value) {
    return;
  }

  const width = githubGlobeRef.value.clientWidth;
  const height = githubGlobeRef.value.clientHeight;

  camera.aspect = width / height;
  camera.updateProjectionMatrix();

  renderer.setSize(width, height);
}
function startAnimation() {
  if (!globe || !globeData.value!) return;
  globe
    .arcsData(props.data)
    .arcStartLat((d: any) => d.startLat * 1)
    .arcStartLng((d: any) => d.startLng * 1)
    .arcEndLat((d: any) => d.endLat * 1)
    .arcEndLng((d: any) => d.endLng * 1)
    .arcColor((e: any) => e.color)
    .arcAltitude((e: any) => e.arcAlt * 1)
    .arcStroke((e: any) => [0.32, 0.28, 0.3][Math.round(Math.random() * 4)])
    .arcDashLength(defaultGlobeConfig.arcLength!)
    .arcDashInitialGap((e: any) => e.order * 1)
    .arcDashGap(15)
    .arcDashAnimateTime(defaultGlobeConfig.arcTime!)

    .pointsData(props.data)
    .pointColor((e: any) => e.color)
    .pointsMerge(true)
    .pointAltitude(0.0)
    .pointRadius(2)

    .ringsData([])
    .ringColor((e: any) => (t: any) => e.color(t))
    .ringMaxRadius(defaultGlobeConfig.maxRings!)
    .ringPropagationSpeed(3)
    .ringRepeatPeriod(
      (defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / defaultGlobeConfig.rings!,
    );
}

function animate() {
  globe.rotation.y += 0.01; // Rotate globe

  renderer.render(scene, camera);

  requestAnimationFrame(animate);
}

function buildData() {
  const arcs = props.data;
  let points = [];
  for (let i = 0; i < arcs.length; i++) {
    const arc = arcs[i];
    const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number };
    points.push({
      size: props.globeConfig.pointSize,
      order: arc.order,
      color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
      lat: arc.startLat,
      lng: arc.startLng,
    });
    points.push({
      size: props.globeConfig.pointSize,
      order: arc.order,
      color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
      lat: arc.endLat,
      lng: arc.endLng,
    });
  }

  // remove duplicates for same lat and lng
  const filteredPoints = points.filter(
    (v, i, a) =>
      a.findIndex((v2) =>
        ["lat", "lng"].every((k) => v2[k as "lat" | "lng"] === v[k as "lat" | "lng"]),
      ) === i,
  );

  globeData.value = filteredPoints;
}

function hexToRgb(color: string) {
  let hex = color.replace(/^#/, "");

  // If the hex code is 3 characters, expand it to 6 characters
  if (hex.length === 3) {
    hex = hex
      .split("")
      .map((char) => char + char)
      .join("");
  }

  // Parse the r, g, b values from the hex string
  const bigint = parseInt(hex, 16);
  const r = (bigint >> 16) & 255; // Extract the red component
  const g = (bigint >> 8) & 255; // Extract the green component
  const b = bigint & 255; // Extract the blue component

  // Return the RGB values as a string separated by spaces
  return {
    r,
    g,
    b,
  };
}

function genRandomNumbers(min: number, max: number, count: number) {
  const arr = [];
  while (arr.length < count) {
    const r = Math.floor(Math.random() * (max - min)) + min;
    if (arr.indexOf(r) === -1) arr.push(r);
  }

  return arr;
}
</script>

```

#### Download GeoJSON file

Download a GeoJSON file containing the globe's geographical data from [GeoJSON Maps](https://geojson-maps.kyd.au/) by customizing the continents and detail level. Save the downloaded file as `globe.json` in the same folder as your component.

::

## API

| Prop Name     | Type         | Default | Description                                                                                         |
| ------------- | ------------ | ------- | --------------------------------------------------------------------------------------------------- |
| `globeConfig` | `object`     | `{}`    | Configuration options for the globe, including colors, atmosphere, rotation speed, and lighting.    |
| `data`        | `Position[]` | `[]`    | Array of positions representing arcs and points on the globe, with latitude, longitude, color, etc. |
| `class`       | `string`     | `""`    | Additional CSS classes for custom styling.                                                          |

### `globeConfig` Properties

| Property             | Type      | Default                 | Description                                              |
| -------------------- | --------- | ----------------------- | -------------------------------------------------------- |
| `pointSize`          | `number`  | `1`                     | Size of individual points on the globe.                  |
| `globeColor`         | `string`  | `"#1d072e"`             | Color of the globe surface.                              |
| `showAtmosphere`     | `boolean` | `true`                  | Whether to display atmosphere around the globe.          |
| `atmosphereColor`    | `string`  | `"#ffffff"`             | Color of the atmosphere around the globe.                |
| `atmosphereAltitude` | `number`  | `0.1`                   | Altitude of the atmosphere layer.                        |
| `emissive`           | `string`  | `"#000000"`             | Emissive color for the globe material.                   |
| `emissiveIntensity`  | `number`  | `0.1`                   | Intensity of the emissive color.                         |
| `shininess`          | `number`  | `0.9`                   | Shininess of the globe material.                         |
| `polygonColor`       | `string`  | `rgba(255,255,255,0.7)` | Color of polygon boundaries on the globe.                |
| `arcTime`            | `number`  | `2000`                  | Duration for arcs animation.                             |
| `arcLength`          | `number`  | `0.9`                   | Length of arcs on the globe.                             |
| `rings`              | `number`  | `1`                     | Number of rings to display per point.                    |
| `maxRings`           | `number`  | `3`                     | Maximum rings around each point.                         |
| `initialPosition`    | `object`  | `{ lat: 0, lng: 0 }`    | Initial latitude and longitude for the globe's position. |
| `autoRotate`         | `boolean` | `false`                 | Automatically rotate the globe.                          |
| `autoRotateSpeed`    | `number`  | `0.8`                   | Speed of auto-rotation when enabled.                     |

### `data` Structure (Position)

| Field      | Type     | Description                                     |
| ---------- | -------- | ----------------------------------------------- |
| `order`    | `number` | Order of the point or arc for sequencing.       |
| `startLat` | `number` | Starting latitude for an arc.                   |
| `startLng` | `number` | Starting longitude for an arc.                  |
| `endLat`   | `number` | Ending latitude for an arc.                     |
| `endLng`   | `number` | Ending longitude for an arc.                    |
| `arcAlt`   | `number` | Altitude of the arc (determines arc height).    |
| `color`    | `string` | Color of the arc or point in hex or RGB format. |

## Features

- **3D Interactive Globe**: A Three.js-based globe with interactive controls and orbit support.
- **Customizable Globe & Atmosphere**: Allows configuration of globe color, atmosphere visibility, and colors.
- **Arcs & Points Visualization**: Display arcs and points on the globe, simulating connections and locations.
- **Auto-Rotate and Zoom**: Supports auto-rotation, zoom, and customizable controls for a dynamic experience.
- **Responsive Design**: Adapts to container size and maintains performance with WebGL rendering.

## Credits

- Built with Three.js and Three Globe libraries, designed for global data visualizations and dynamic effects.
- Ported from [Aceternity UI](https://ui.aceternity.com/components/github-globe).

URL: https://inspira-ui.com/components/visualization/globe

---
title: Globe
description: An interactive rotating globe component.
---

```vue
<template>
  <div
    class="relative flex size-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background px-40 pb-40 pt-8 md:pb-60 md:shadow-xl"
  >
    <span
      class="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent max-lg:-mt-12 dark:from-white dark:to-slate-900/10"
    >
      Globe
    </span>
    <Globe class="top-28" />
    <div
      class="pointer-events-none absolute inset-0 h-full bg-[radial-gradient(circle_at_50%_200%,rgba(0,0,0,0.2),rgba(255,255,255,0))]"
    />
  </div>
</template>

```

::alert{type="info"}
**Note:** This component uses `cobe` and `vue-use-spring` as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="globe" />
```

## Install Manually

::steps{level=4}

#### Install the dependencies

::code-group

```bash [npm]
npm install cobe vue-use-spring
```

```bash [pnpm]
pnpm install cobe vue-use-spring
```

```bash [bun]
bun add cobe vue-use-spring
```

```bash [yarn]
yarn add cobe vue-use-spring
```

::

Copy and paste the following code

```vue
<template>
  <div :class="cn('absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]', $props.class)">
    <canvas
      ref="globeCanvasRef"
      class="size-full opacity-0 transition-opacity duration-1000 ease-in-out [contain:layout_paint_size]"
      @pointerdown="(e) => updatePointerInteraction(e.clientX)"
      @pointerup="updatePointerInteraction(null)"
      @pointerout="updatePointerInteraction(null)"
      @mousemove="(e) => updateMovement(e.clientX)"
      @touchmove="(e) => e.touches[0] && updateMovement(e.touches[0].clientX)"
    ></canvas>
  </div>
</template>

<script lang="ts" setup>
import { cn } from "@/lib/utils";
import createGlobe, { type COBEOptions } from "cobe";
import { useSpring } from "vue-use-spring";
import { ref, onMounted, onBeforeUnmount } from "vue";

type GlobeProps = {
  class?: string;
  config?: Partial<COBEOptions>;
  mass?: number;
  tension?: number;
  friction?: number;
  precision?: number;
};

const DEFAULT_CONFIG: COBEOptions = {
  width: 800,
  height: 800,
  onRender: () => {},
  devicePixelRatio: 2,
  phi: 0,
  theta: 0.3,
  dark: 0,
  diffuse: 0.4,
  mapSamples: 16000,
  mapBrightness: 1.2,
  baseColor: [1, 1, 1],
  markerColor: [251 / 255, 100 / 255, 21 / 255],
  glowColor: [1.2, 1.2, 1.2],
  markers: [
    { location: [14.5995, 120.9842], size: 0.03 },
    { location: [19.076, 72.8777], size: 0.1 },
    { location: [23.8103, 90.4125], size: 0.05 },
    { location: [30.0444, 31.2357], size: 0.07 },
    { location: [39.9042, 116.4074], size: 0.08 },
    { location: [-23.5505, -46.6333], size: 0.1 },
    { location: [19.4326, -99.1332], size: 0.1 },
    { location: [40.7128, -74.006], size: 0.1 },
    { location: [34.6937, 135.5022], size: 0.05 },
    { location: [41.0082, 28.9784], size: 0.06 },
  ],
};

const props = withDefaults(defineProps<GlobeProps>(), {
  mass: 1,
  tension: 280,
  friction: 100,
  precision: 0.001,
});

const globeCanvasRef = ref<HTMLCanvasElement>();
const phi = ref(0);
const width = ref(0);
const pointerInteracting = ref();
const pointerInteractionMovement = ref();

let globe: ReturnType<typeof createGlobe> | null = null;

const spring = useSpring(
  {
    r: 0,
  },
  {
    mass: props.mass,
    tension: props.tension,
    friction: props.friction,
    precision: props.precision,
  },
);

function updatePointerInteraction(clientX: number | null) {
  if (clientX !== null) {
    pointerInteracting.value = clientX - (pointerInteractionMovement.value ?? clientX);
  } else {
    pointerInteracting.value = null;
  }

  if (globeCanvasRef.value) {
    globeCanvasRef.value.style.cursor = clientX ? "grabbing" : "grab";
  }
}

function updateMovement(clientX: number) {
  if (pointerInteracting.value !== null) {
    const delta = clientX - (pointerInteracting.value ?? clientX);
    pointerInteractionMovement.value = delta;
    spring.r = delta / 200;
  }
}

function onRender(state: Record<string, unknown>) {
  if (!pointerInteracting.value) {
    phi.value += 0.005;
  }

  state.phi = phi.value + spring.r;
  state.width = width.value * 2;
  state.height = width.value * 2;
}

function onResize() {
  if (globeCanvasRef.value) {
    width.value = globeCanvasRef.value.offsetWidth;
  }
}

function createGlobeOnMounted() {
  const config = { ...DEFAULT_CONFIG, ...props.config };

  globe = createGlobe(globeCanvasRef.value!, {
    ...config,
    width: width.value * 2,
    height: width.value * 2,
    onRender,
  });
}

onMounted(() => {
  window.addEventListener("resize", onResize);
  onResize();
  createGlobeOnMounted();

  setTimeout(() => (globeCanvasRef.value!.style.opacity = "1"));
});

onBeforeUnmount(() => {
  globe?.destroy();
  window.removeEventListener("resize", onResize);
});
</script>

```
::

## API

| Prop Name   | Type          | Default | Description                                                                                                 |
| ----------- | ------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `class`     | `string`      | `""`    | Additional CSS classes for custom styling.                                                                  |
| `config`    | `COBEOptions` | N/A     | Configuration object for the globe, following **[COBE]**(https://cobe.vercel.app/docs/api) library options. |
| `mass`      | `number`      | `1`     | Mass parameter for the spring animation controlling rotation inertia.                                       |
| `tension`   | `number`      | `280`   | Tension parameter for the spring animation, affecting responsiveness.                                       |
| `friction`  | `number`      | `100`   | Friction parameter for the spring animation, affecting damping.                                             |
| `precision` | `number`      | `0.001` | Precision parameter for the spring animation calculations.                                                  |

## Features

- **Interactive 3D Globe**: Provides an interactive 3D globe visualization that users can rotate with mouse or touch interactions.

- **Customizable Markers**: Display markers on the globe at specified locations with customizable sizes.

- **Smooth Animations**: Utilizes physics-based spring animations for smooth and responsive globe rotations.

- **Configurable Appearance**: Customize the globe's appearance and behavior through the `config` prop using COBE library options.

- **Responsive Design**: Automatically adjusts to different screen sizes and resolutions.

## Credits

- Built using the [cobe](https://github.com/shuding/cobe) library for WebGL globe visualization.

- Ported from [Magic UI](https://magicui.design/docs/components/globe).

URL: https://inspira-ui.com/components/visualization/icon-cloud

---
title: Icon Cloud
description: An interactive 3D tag cloud component
---

```vue
<template>
  <div class="grid place-content-center p-6">
    <IconCloud :images="imageUrls" />
  </div>
</template>

<script setup lang="ts">
const slugs = [
  "typescript",
  "javascript",
  "dart",
  "java",
  "react",
  "flutter",
  "android",
  "html5",
  "css3",
  "nodedotjs",
  "express",
  "nextdotjs",
  "prisma",
  "amazonaws",
  "postgresql",
  "firebase",
  "nginx",
  "vercel",
  "testinglibrary",
  "jest",
  "cypress",
  "docker",
  "git",
  "jira",
  "github",
  "gitlab",
  "visualstudiocode",
  "androidstudio",
  "sonarqube",
  "figma",
];

const imageUrls = slugs.map((slug) => `https://cdn.simpleicons.org/${slug}/${slug}`);
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="icon-cloud" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="IconCloud.vue" language="vue" componentName="IconCloud" type="ui" id="icon-cloud"}
:CodeViewerTab{filename="index.ts" language="typescript" componentName="index" type="ui" id="icon-cloud" extension="ts"}

::

## API

| Prop Name | Type     | Default | Description                                   |
| --------- | -------- | ------- | --------------------------------------------- |
| `class`   | `string` | -       | Additional classes for styling the component. |
| `images`  | `array`  | `[]`    | Array of image URLs to render in the cloud    |

## Credits

- Inspired by [MagicUI](https://magicui.design/docs/components/icon-cloud).
- Credits to [kalix127](https://github.com/kalix127) for porting this component.

URL: https://inspira-ui.com/components/visualization/liquid-logo

---
title: Liquid Logo
description: An advanced WebGL-based component that applies a dynamic, liquid effect to logos or images using custom shaders.
navBadges:
  - value: New
    type: lime
---

```vue
<template>
  <div class="flex h-96 w-full items-center justify-center">
    <LiquidLogo :image-url="imageUrl" />
  </div>
</template>

<script lang="ts" setup>
const imageUrl = "https://inspira-ui.com/images/apple-logo.svg";
</script>

```

## API

| Prop Name      | Type     | Default | Description                                  |
| -------------- | -------- | ------- | -------------------------------------------- |
| `class`        | `string` | `""`    | Additional CSS classes for custom styling.   |
| `imageUrl`     | `string` | `""`    | URL of the image to apply the liquid effect. |
| `patternScale` | `number` | `2`     | Scale of the distortion pattern.             |
| `refraction`   | `number` | `0.015` | Amount of refraction applied to the image.   |
| `edge`         | `number` | `0.4`   | Sharpness of the edge effect.                |
| `patternBlur`  | `number` | `0.005` | Blur effect applied to the pattern.          |
| `liquid`       | `number` | `0.07`  | Intensity of the liquid animation.           |
| `speed`        | `number` | `0.3`   | Animation speed of the liquid effect.        |

## Component Code

You can copy and paste the following code to create this component:

::code-group

    ::CodeViewerTab{label="LiquidLogo.vue" language="vue" componentName="LiquidLogo" type="ui" id="liquid-logo"}
    ::

    ::CodeViewerTab{label="parseLogoImage.ts" language="typescript" componentName="parseLogoImage" type="ui" id="liquid-logo" extension="ts"}
    ::

    ::CodeViewerTab{label="shader.ts" language="typescript" componentName="shader" type="ui" id="liquid-logo" extension="ts"}
    ::

::

## Features

- **Dynamic Liquid Effect**: Transforms logos or images with a fluid, liquid-like motion.
- **Custom Shader Utilization**: Built with WebGL2 and GLSL shaders for real-time rendering.
- **Interactive Animations**: Smooth and configurable animation effects.
- **Responsive Scaling**: Automatically adjusts to different screen sizes and resolutions.
- **Configurable Parameters**: Offers a wide range of props to control effects like refraction, speed, and edge sharpness.

## Credits

- Inspired by the Apple Fluid Motion design.
- Ported and enhaced from [Paper Design Concept](https://github.com/paper-design/liquid-logo).

URL: https://inspira-ui.com/components/visualization/logo-cloud

---
title: Animated Logo Cloud
description: Animated company cloud logs. Usually we can use to show company logos.
---

```vue
<template>
  <div>
    <StaticLogoCloud
      :logos
      title="Sponsored By"
    />
  </div>
</template>

<script lang="ts" setup>
const logos = [
  {
    name: "Vercel",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715881430/vercel_wordmark_dark_mhv8u8.svg",
  },
  {
    name: "Trustpilot",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276558/logos/tkfspxqmjflfllbuqxsi.svg",
  },
  {
    name: "Webflow",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276560/logos/nymiivu48d5lywhf9rpf.svg",
  },
  {
    name: "Airbnb",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276558/logos/pmblusboe7vkw8vxdknx.svg",
  },
  {
    name: "Tina",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276560/logos/afqhiygywyphuou6xtxc.svg",
  },
];
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="logo-cloud" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="AnimatedLogoCloud.vue" language="vue" componentName="AnimatedLogoCloud" type="ui" id="logo-cloud"}
:CodeViewerTab{label="IconLogoCloud.vue" language="vue" componentName="IconLogoCloud" type="ui" id="logo-cloud"}
:CodeViewerTab{label="StaticLogoCloud.vue" language="vue" componentName="StaticLogoCloud" type="ui" id="logo-cloud"}
:CodeViewerTab{filename="index.ts" language="typescript" componentName="index" type="ui" id="logo-cloud" extension="ts"}

::

## Examples

### Animated logs

Animated company logos.

```vue
<template>
  <div>
    <AnimatedLogoCloud
      :logos
      title="Trusted by Companies like"
    />
  </div>
</template>

<script lang="ts" setup>
const logos = [
  {
    name: "Vercel",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715881430/vercel_wordmark_dark_mhv8u8.svg",
  },
  {
    name: "Prime",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276558/logos/t2awrrfzdvmg1chnzyfr.svg",
  },
  {
    name: "Trustpilot",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276558/logos/tkfspxqmjflfllbuqxsi.svg",
  },
  {
    name: "Webflow",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276560/logos/nymiivu48d5lywhf9rpf.svg",
  },
  {
    name: "Airbnb",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276558/logos/pmblusboe7vkw8vxdknx.svg",
  },
  {
    name: "Tina",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276560/logos/afqhiygywyphuou6xtxc.svg",
  },
  {
    name: "Stackoverflow",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276558/logos/ts1j4mkooxqmscgptafa.svg",
  },
  {
    name: "mistral",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1715276558/logos/tyos2ayezryjskox3wzs.svg",
  },
];
</script>

```

### Icon Logo

Company logo icons

```vue
<template>
  <div>
    <IconLogoCloud
      :logos
      title="Trusted by Companies like"
    />
  </div>
</template>

<script lang="ts" setup>
const logos = [
  {
    name: "Hume AI",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724242869/hume-ai_sxfeu8.svg",
  },
  {
    name: "Supabase",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724242819/supabase_bdbnvy.svg",
  },
  {
    name: "Apple",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724242912/apple_dark_wixhyc.svg",
  },
  {
    name: "Asana",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724243064/asana-logo_ulshgt.svg",
  },
  {
    name: "DigitalOcean",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724243142/digitalocean_werjgx.svg",
  },
  {
    name: "Github",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724243168/github-dark_u5eygu.svg",
  },
  {
    name: "Linear",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724243194/linear_dtq4zq.svg",
  },
  {
    name: "Loom",
    path: "https://res.cloudinary.com/dfhp33ufc/image/upload/v1724243196/loom_ehvtka.svg",
  },
];
</script>

```

## API

| Prop Name | Type     | Default                     | Description                                                    |
| --------- | -------- | --------------------------- | -------------------------------------------------------------- |
| `class`   | `string` | `-`                         | The delay in milliseconds before adding each item to the list. |
| `title`   | `string` | `Trusted by Companies like` | Title of animated logs.                                        |
| `logos`   | `[]`     | `[{name: "", path: ""}]`    | Array of items(logos) with name & path fields.                 |

## Credits

- Credits to [SivaReddy Uppathi](https://github.com/sivareddyuppathi) for this component.

URL: https://inspira-ui.com/components/visualization/logo-origami

---
title: Logo Origami
description: Animated flipping logo with origami effect.
---

```vue
<template>
  <div
    class="flex h-72 flex-col items-center justify-center gap-12 bg-background px-4 py-24 md:flex-row"
  >
    <LogoOrigami>
      <LogoOrigamiItem class="bg-orange-300 text-neutral-900">
        <Icon
          name="mdi:github"
          size="96"
        />
      </LogoOrigamiItem>
      <LogoOrigamiItem class="bg-green-300 text-neutral-900">
        <Icon
          name="logos:notion-icon"
          size="96"
        />
      </LogoOrigamiItem>
      <LogoOrigamiItem class="bg-cyan-300 text-neutral-900">
        <Icon
          name="logos:whatsapp-icon"
          size="96"
        />
      </LogoOrigamiItem>
      <LogoOrigamiItem class="bg-purple-300 text-black">
        <Icon
          name="logos:messenger"
          size="96"
        />
      </LogoOrigamiItem>
      <LogoOrigamiItem class="bg-white text-neutral-900">
        <Icon
          name="logos:google-drive"
          size="96"
        />
      </LogoOrigamiItem>
    </LogoOrigami>
  </div>
</template>

```

## Install using CLI

```vue
<InstallationCli component-id="logo-origami" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="LogoOrigami.vue" language="vue" componentName="LogoOrigami" type="ui" id="logo-origami"}
:CodeViewerTab{filename="LogoOrigamiItem.vue" language="vue" componentName="LogoOrigamiItem" type="ui" id="logo-origami"}
::

## API

| Prop Name  | Type     | Default | Description                                |
| ---------- | -------- | ------- | ------------------------------------------ |
| `class`    | `string` | `""`    | Additional CSS classes for custom styling. |
| `duration` | `number` | `1.5`   | Duration of the flip animation in seconds. |
| `delay`    | `number` | `2.5`   | Delay between flip animations in seconds.  |

## Features

- **Origami Flip Animation**: Animates flipping between multiple child components with an origami-style effect.

- **Customizable Timing**: Adjust the `duration` and `delay` props to control the animation speed and interval.

- **Slot-Based Content**: Accepts multiple child components or content via default slots.

- **Responsive Design**: Designed to adapt to various screen sizes and devices.

- **Easy Integration**: Simple to include in your Vue projects with minimal setup.

## Credits

- Inspired by origami animations and flip effects at [hover.dev](www.hover.dev/components/other#logo-origami)

URL: https://inspira-ui.com/components/visualization/orbit

---
title: Orbit
description: A component that animates content in a circular orbit, with customizable duration, delay, and radius. It also offers an optional orbit path display.
---

```vue
<template>
  <div
    class="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
  >
    <span class="pointer-events-none text-center text-8xl font-semibold leading-none"> 🌍 </span>

    <!-- Inner Circles -->
    <Orbit
      class="size-[30px] items-center justify-center border-none bg-transparent"
      :duration="20"
      :delay="20"
      :radius="80"
      :direction="ORBIT_DIRECTION.CounterClockwise"
    >
      <Icon
        name="logos:whatsapp-icon"
        size="24"
      />
    </Orbit>
    <Orbit
      class="size-[30px] items-center justify-center border-none bg-transparent"
      :duration="20"
      :delay="10"
      :radius="80"
      path
      :direction="ORBIT_DIRECTION.CounterClockwise"
    >
      <Icon
        name="logos:notion-icon"
        size="24"
      />
    </Orbit>

    <!-- Outer Circles (reverse) -->
    <Orbit
      class="size-[50px] items-center justify-center border-none bg-transparent"
      :radius="190"
      :duration="20"
      path
    >
      <Icon
        name="logos:google-drive"
        size="50"
      />
    </Orbit>
    <Orbit
      class="size-[50px] items-center justify-center border-none bg-transparent"
      :radius="190"
      :duration="20"
      :delay="200"
      :direction="ORBIT_DIRECTION.CounterClockwise"
    >
      <Icon
        name="mdi:github"
        size="50"
      />
    </Orbit>
    <Orbit
      class="items-center justify-center border-none bg-transparent text-4xl"
      :radius="140"
      :delay="4"
    >
      🌕
    </Orbit>
  </div>
</template>

<script setup lang="ts">
import { ORBIT_DIRECTION } from "../ui/orbit";
</script>

```

## Install using CLI

```vue
<InstallationCli component-id="orbit" />
```

## Install Manually

Copy and paste the following code in the same folder

::code-group

:CodeViewerTab{label="Orbit.vue" language="vue" componentName="Orbit" type="ui" id="orbit"}
:CodeViewerTab{filename="index.ts" language="typescript" componentName="index" type="ui" id="orbit" extension="ts"}

::

## Examples

Synchronized orbit

```vue
<template>
  <div class="flex w-full flex-row items-center justify-between py-4">
    <p>
      {{
        `Current direction : ${ORBIT_DIRECTION.Clockwise === direction ? "Clockwise" : "CounterClockwise"}`
      }}
    </p>
    <button
      class="rounded-md bg-black px-4 py-2 text-sm font-medium text-white dark:bg-white dark:text-black"
      @click="switchDirection"
    >
      {{
        `Switch to : ${ORBIT_DIRECTION.Clockwise === direction ? "CounterClockwise" : "Clockwise"}`
      }}
    </button>
  </div>
  <div
    class="relative flex h-[600px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
  >
    <span class="pointer-events-none text-center text-8xl font-semibold leading-none"> 🌍 </span>

    <Orbit
      class="items-center justify-center border-none bg-transparent text-xl"
      :radius="190"
      :duration="20"
      :delay="200"
      :direction="direction"
      path
    >
      🪨
    </Orbit>
    <Orbit
      class="items-center justify-center border-none bg-transparent text-4xl"
      :radius="100"
      :delay="4"
      :direction="direction"
      path
    >
      🌕
    </Orbit>
    <Orbit
      class="items-center justify-center border-none bg-transparent text-4xl"
      :radius="250"
      :delay="4"
      :direction="direction"
      path
    >
      🪐
    </Orbit>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { ORBIT_DIRECTION, type OrbitDirection } from "../ui/orbit";

const direction = ref<OrbitDirection>(ORBIT_DIRECTION.Clockwise);

function switchDirection() {
  if (ORBIT_DIRECTION.Clockwise === direction.value) {
    direction.value = ORBIT_DIRECTION.CounterClockwise;
    return;
  }

  direction.value = ORBIT_DIRECTION.Clockwise;
}
</script>

```

## API

| Prop Name   | Type                  | Default  | Description                                                           |
| ----------- | --------------------- | -------- | --------------------------------------------------------------------- |
| `direction` | `normal` \| `reverse` | `normal` | The direction of the orbit. You can use the constant ORBIT_DIRECTION. |
| `duration`  | `?number`             | `20`     | The duration of the orbit animation in seconds.                       |
| `delay`     | `?number`             | `10`     | Delay in seconds before the animation starts.                         |
| `radius`    | `?number`             | `50`     | Radius of the orbit path in pixels.                                   |
| `path`      | `?boolean`            | `false`  | Displays a circle path for the orbit if `true`.                       |

## Features

- **Circular Orbit Animation**: The component animates its content in a smooth circular orbit around the center point.
- **Customizable Animation**: The orbit’s duration, delay, and radius are fully adjustable, providing flexibility in animation behavior.

- **Optional Orbit Path**: An optional visual representation of the orbit path can be toggled using the `path` prop.

- **Reversibility**: The orbit direction can be `reverse` by setting the `direction` prop.

- **Responsive and Efficient**: The component handles different container sizes and uses Vue’s reactivity to ensure efficient animation.

## Credits

- Inspired by [Magic UI](https://magicui.design/docs/components/orbiting-circles).
- Credits to [Nathan De Pachtere](https://nathandepachtere.com/) for updating this component.

URL: https://inspira-ui.com/components/visualization/spline

---
title: Spline
description: A Vue wrapper component for the Spline 3D tool, providing events and auto-resizing.
navBadges:
  - value: New
    type: lime
---

```vue
<template>
  <div class="relative w-full overflow-hidden rounded-lg bg-black/[0.96]">
    <div
      class="absolute flex w-full flex-col items-center justify-center gap-2 p-8 text-center font-heading text-white"
    >
      <span class="text-4xl font-semibold"> Inspira UI </span>
      <span class="font-sans font-light">Build spline animations with style.</span>
    </div>
    <div class="flex">
      <Spline
        :scene="sceneUrl"
        class="mt-24 size-full"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
const sceneUrl = "https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode";
</script>

```

## API

| Prop Name        | Type       | Default     | Description                                                   |
| ---------------- | ---------- | ----------- | ------------------------------------------------------------- |
| `scene`          | `string`   | —           | The URL or path to the Spline scene file. **Required**.       |
| `onLoad`         | `Function` | `undefined` | Callback that fires when the Spline scene loads successfully. |
| `renderOnDemand` | `boolean`  | `true`      | Enables or disables Spline's render-on-demand optimization.   |
| `style`          | `object`   | `{}`        | Custom CSS style applied to the container of the canvas.      |

**Emits**

| Event Name           | Payload | Description                                                   |
| -------------------- | ------- | ------------------------------------------------------------- |
| `error`              | `Error` | Emits if there's an error while loading the Spline scene.     |
| `spline-mouse-down`  | `any`   | Emits when a mouseDown event is detected in the Spline scene. |
| `spline-mouse-up`    | `any`   | Emits when a mouseUp event is detected in the Spline scene.   |
| `spline-mouse-hover` | `any`   | Emits when the mouseHover event is triggered.                 |
| `spline-key-down`    | `any`   | Emits on keyDown within the Spline scene.                     |
| `spline-key-up`      | `any`   | Emits on keyUp within the Spline scene.                       |
| `spline-start`       | `any`   | Emits when the Spline scene starts.                           |
| `spline-look-at`     | `any`   | Emits when a lookAt event occurs.                             |
| `spline-follow`      | `any`   | Emits when a follow event occurs.                             |
| `spline-scroll`      | `any`   | Emits on scroll interactions.                                 |

::alert{type="info"}
**Note:** This component uses Spline & requires `@splinetool/runtime` npm package as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="spline" />
```

## Install Manually

### Install the dependencies

::code-group

```bash [npm]
npm install @splinetool/runtime
```

```bash [pnpm]
pnpm install @splinetool/runtime
```

```bash [bun]
bun add @splinetool/runtime
```

```bash [yarn]
yarn add @splinetool/runtime
```

::

### Component Code

You can copy and paste the following code to create this component:

::code-group

::CodeViewerTab{label="Spline.vue" language="vue" componentName="Spline" type="ui" id="spline"}
::

::CodeViewerTab{label="ParentSize.vue" language="vue" componentName="ParentSize" type="ui" id="spline"}
::

::

## Features

- **Responsive Canvas**: Uses a `ParentSize` wrapper to adjust to the parent container size.
- **Event Bindings**: Exposes Spline’s mouse, keyboard, and scrolling events via Vue emits.
- **Render-On-Demand**: Optionally only re-renders when necessary, optimizing performance.
- **Easy Integration**: Load a Spline 3D scene by simply passing the `scene` prop.
- **Cleanup & Disposal**: Manages resource disposal on unmount to avoid memory leaks.

## Credits

- Utilizes Spline’s runtime behind the scenes.
- Inspired by various 3D web experiences using Spline.

URL: https://inspira-ui.com/components/visualization/world-map

---
title: World Map
description: Displays a customizable world map with animated arcs and pulse effects for geographical points.
navBadges:
  - value: New
    type: lime
---

```vue
<template>
  <div class="w-full bg-white py-40 dark:bg-black">
    <div class="mx-auto max-w-7xl text-center">
      <p class="text-xl font-bold text-black md:text-4xl dark:text-white">
        Remote
        <span class="text-neutral-400">
          <Motion
            v-for="(word, idx) in 'Connectivity'.split('')"
            :key="idx"
            as="span"
            class="inline-block"
            :initial="{ x: -10, opacity: 0 }"
            :animate="{ x: 0, opacity: 1 }"
            :transition="{ duration: 0.5, delay: idx * 0.04 }"
          >
            {{ word }}
          </Motion>
        </span>
      </p>
      <p class="mx-auto max-w-2xl py-4 text-sm text-neutral-500 md:text-lg">
        Break free from traditional boundaries. Work from anywhere, at the comfort of your own
        studio apartment. Perfect for Nomads and Travellers.
      </p>
    </div>
    <WorldMap
      :dots="dots"
      :map-color="isDark ? '#FFFFFF40' : '#00000040'"
      :map-bg-color="isDark ? 'black' : 'white'"
    />
  </div>
</template>

<script lang="ts" setup>
import { Motion } from "motion-v";

const dots = [
  {
    start: {
      lat: 64.2008,
      lng: -149.4937,
    }, // Alaska (Fairbanks)
    end: {
      lat: 34.0522,
      lng: -118.2437,
    }, // Los Angeles
  },
  {
    start: { lat: 64.2008, lng: -149.4937 }, // Alaska (Fairbanks)
    end: { lat: -15.7975, lng: -47.8919 }, // Brazil (Brasília)
  },
  {
    start: { lat: -15.7975, lng: -47.8919 }, // Brazil (Brasília)
    end: { lat: 38.7223, lng: -9.1393 }, // Lisbon
  },
  {
    start: { lat: 51.5074, lng: -0.1278 }, // London
    end: { lat: 28.6139, lng: 77.209 }, // New Delhi
  },
  {
    start: { lat: 28.6139, lng: 77.209 }, // New Delhi
    end: { lat: 43.1332, lng: 131.9113 }, // Vladivostok
  },
  {
    start: { lat: 28.6139, lng: 77.209 }, // New Delhi
    end: { lat: -1.2921, lng: 36.8219 }, // Nairobi
  },
];

const isDark = computed(() => useColorMode().value == "dark");
</script>

```

## API

| Prop Name    | Type                                                                                                                 | Default     | Description                                                                             |
| ------------ | -------------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------- |
| `dots`       | `Array<{ start: { lat: number; lng: number; label?: string }, end: { lat: number; lng: number; label?: string } }> ` | `[]`        | Array of dot objects, each containing a start and end coordinate (latitude, longitude). |
| `class`      | `string`                                                                                                             | `""`        | Additional CSS classes for custom styling.                                              |
| `lineColor`  | `string`                                                                                                             | `"#0EA5E9"` | Color of the arcs and dot borders.                                                      |
| `mapColor`   | `string`                                                                                                             | —           | Main color of the dotted map. (**Required**)                                            |
| `mapBgColor` | `string`                                                                                                             | —           | Background color of the map. (**Required**)                                             |

::alert{type="info"}
**Note:** This component uses Spline & requires `dotted-map` npm package as a dependency.
::

## Install using CLI

```vue
<InstallationCli component-id="world-map" />
```

## Install Manually

### Install the dependencies

::code-group

```bash [npm]
npm install dotted-map
```

```bash [pnpm]
pnpm install dotted-map
```

```bash [bun]
bun add dotted-map
```

```bash [yarn]
yarn add dotted-map
```

::

## Component Code

You can copy and paste the following code to create this component:

::code-group

::CodeViewerTab{label="WorldMap.vue" language="vue" componentName="WorldMap" type="ui" id="world-map"}
::

::

## Features

- **Animated Arcs**: Smooth arcs between two geographical points using an SVG `path`.
- **Dotted Background**: Renders a dotted map using the DottedMap library.
- **Pulsing Markers**: Each point animates with expanding circles to highlight the location.
- **Dynamic Projection**: Projects latitude/longitude to an 800x400 coordinate plane.
- **Customizable Colors**: Control lineColor, mapColor, and mapBgColor.

## Credits

- Ported from (World Map by Aceternity UI)[https://ui.aceternity.com/components/world-map].

URL: https://inspira-ui.com/blocks/index

---
title: Block Index
description: List of all the blocks provided by Inspira UI.
navigation: false
---

## All Blocks

::ComponentIndex{path="blocks"}
::

URL: https://inspira-ui.com/blocks/miscellaneous/hero

---
title: Hero
description: Elevate website experience with unique hero blocks.
---

### Hero Section 1

```vue
<template>
  <section class="relative flex min-h-screen w-full flex-col items-center justify-center">
    <FallingStarsBg class="bg-black" />
    <div class="absolute inset-0 z-[1] bg-black opacity-50"></div>
    <div class="z-[2] flex max-w-xl flex-col items-center gap-2">
      <BlurReveal
        :delay="0.5"
        class="flex flex-col items-center justify-center gap-4"
      >
        <span
          class="flex flex-col items-center justify-center bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text text-center text-4xl font-bold text-transparent md:text-6xl"
        >
          <span>
            Building
            <FlipWords
              :words="['beautiful', 'stunning', 'amazing']"
              :duration="3000"
              class="w-52 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text text-transparent"
            />
          </span>
          web experience
        </span>

        <span class="text-center text-xl">
          Inspira UI is the new way to build beautiful website
        </span>

        <div class="my-2 flex flex-row items-center justify-center gap-4">
          <UiButton to="/components"> Get Started </UiButton>
          <UiButton
            to="/blocks"
            variant="secondary"
          >
            Learn More
          </UiButton>
        </div>

        <div
          class="relative flex h-fit w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background p-px md:shadow-xl"
        >
          <img
            src="/images/Inspira-dark.png"
            class="w-full rounded-md"
          />
          <BorderBeam
            :size="250"
            :duration="12"
            :delay="9"
            :border-width="2"
          />
        </div>
      </BlurReveal>
    </div>
  </section>
</template>

```

#### Components used:

- [Falling Stars Background](https://inspira-ui.com/components/backgrounds/falling-stars)
- [Blur Reveal](https://inspira-ui.com/components/text-animations/blur-reveal)
- [Flip Words](https://inspira-ui.com/components/text-animations/flip-words)
- [Border Beam](https://inspira-ui.com/components/special-effects/border-beam)

### Hero Section 2

```vue
<template>
  <section class="relative mx-auto h-screen w-full overflow-hidden">
    <Vortex
      background-color="black"
      :range-y="800"
      :particle-count="500"
      :base-hue="120"
      class="absolute inset-0 flex items-center justify-center px-8"
    >
      <div class="flex size-full max-w-6xl flex-col md:flex-row">
        <div
          class="flex size-full h-[36rem] flex-col items-center justify-center md:h-full md:items-start md:p-8"
        >
          <span
            class="inline-block gap-2 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text text-center text-5xl font-bold text-transparent md:text-left md:text-6xl"
          >
            Build beautiful websites with
            <TextHighlight
              class="rounded-xl bg-gradient-to-r from-pink-500 to-violet-500 px-4 py-1"
              text-end-color="hsl(var(--accent))"
            >
              Inspira UI
            </TextHighlight>
          </span>
          <span class="flex w-full flex-row justify-center py-6 md:justify-start">
            <AnimatedTooltip :items="people" />
          </span>
          <UiButton>Get Started</UiButton>
        </div>
        <div class="relative flex flex-col items-center justify-center overflow-hidden md:max-w-96">
          <div class="relative flex size-fit flex-col items-center justify-center rounded-3xl">
            <CardContainer>
              <CardBody
                class="group/card h-fit w-full gap-2 rounded-xl border border-black/[0.1] bg-gray-50 px-4 py-6 dark:border-white/[0.2] dark:bg-black dark:hover:shadow-2xl dark:hover:shadow-emerald-500/[0.1]"
              >
                <CardItem
                  :translate-z="25"
                  class="mb-4 w-full"
                >
                  <NuxtImg
                    src="/og-image.png"
                    height="auto"
                    width="auto"
                    class="h-24 w-full rounded-xl object-cover group-hover/card:shadow-xl"
                    alt="logo"
                    crossorigin="anonymous"
                  />
                </CardItem>

                <CardItem
                  as="p"
                  translate-z="25"
                  class="mt-2 max-w-sm text-sm text-neutral-500 dark:text-neutral-300"
                >
                  Subscribe to newletter & get latest update from Inspira UI.
                </CardItem>

                <CardItem class="mt-4 flex w-full flex-col gap-4">
                  <IInput placeholder="Enter email"></IInput>
                  <UiButton>Sign Me Up!</UiButton>
                </CardItem>
              </CardBody>
            </CardContainer>
            <BorderBeam
              :size="250"
              :duration="12"
              :delay="9"
              :border-width="2"
            />
          </div>
        </div>
      </div>
    </Vortex>
  </section>
</template>

<script lang="ts" setup>
const people = [
  {
    id: 1,
    name: "John Doe",
    designation: "Software Engineer",
    image:
      "https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3387&q=80",
  },
  {
    id: 2,
    name: "Robert Johnson",
    designation: "Product Manager",
    image:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60",
  },
  {
    id: 3,
    name: "Jane Smith",
    designation: "Data Scientist",
    image:
      "https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60",
  },
  {
    id: 4,
    name: "Emily Davis",
    designation: "UX Designer",
    image:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60",
  },
  {
    id: 5,
    name: "Tyler Durden",
    designation: "Soap Developer",
    image:
      "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3540&q=80",
  },
  {
    id: 6,
    name: "Dora",
    designation: "The Explorer",
    image:
      "https://images.unsplash.com/photo-1544725176-7c40e5a71c5e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3534&q=80",
  },
];
</script>

```

#### Components used:

- [Vortex Background](https://inspira-ui.com/components/backgrounds/vortex)
- [Animated Tooltip](https://inspira-ui.com/components/miscellaneous/animated-tooltip)
- [Text Highlight](https://inspira-ui.com/components/text-animations/text-highlight)
- [3D Card](https://inspira-ui.com/components/cards/3d-card)

URL: https://inspira-ui.com/blocks/miscellaneous/testimonials

---
title: Testimonials
description: Show testimonials in style
---

### With skewed marquee

```vue
<template>
  <div class="flex w-full max-w-7xl flex-col items-center justify-center p-4">
    <div class="py-12 text-3xl font-semibold sm:text-4xl">
      Loved by community <span class="text-4xl"> ❤️</span>
    </div>
    <div
      class="relative h-[430px] w-full overflow-hidden rounded-xl border bg-white shadow-lg dark:bg-background"
    >
      <!-- Logo -->
      <div
        class="absolute left-1/2 top-8 z-20 my-4 -translate-x-1/2 rounded-3xl border bg-white/30 p-3 backdrop-blur-md"
      >
        <ClientOnly>
          <NuxtImg
            v-if="isDark"
            src="/logo-dark.svg"
          />
          <NuxtImg
            v-else
            src="/logo.svg"
          />
        </ClientOnly>
      </div>

      <!-- Center Text -->
      <div
        class="absolute inset-0 z-10 mt-20 flex flex-col items-center justify-center px-4 text-center"
      >
        <h3 class="mb-2 text-2xl font-bold sm:text-3xl">What are you waiting for?</h3>
        <p class="m-4 text-base sm:text-lg">Get started and start building awesome UI 😄</p>
        <NuxtLink to="getting-started/installation">
          <UiButton variant="default"> Get Started → </UiButton>
        </NuxtLink>
      </div>

      <!-- Tilted Marquees -->
      <div class="absolute inset-0 overflow-hidden">
        <Marquee
          :style="{ transform: 'translateY(-11.5rem) rotate(-16deg)' }"
          class="marquee"
          :pause-on-hover="false"
        >
          <ReviewCard
            v-for="review in firstRow"
            :key="review.username"
            :img="review.img"
            :name="review.name"
            :username="review.username"
            :body="review.body"
          />
        </Marquee>

        <Marquee
          :style="{ transform: 'translateY(1rem) rotate(-16deg)' }"
          reverse
          class="marquee"
          :pause-on-hover="false"
        >
          <ReviewCard
            v-for="review in firstRow"
            :key="review.username"
            :img="review.img"
            :name="review.name"
            :username="review.username"
            :body="review.body"
          />
        </Marquee>

        <Marquee
          :style="{ transform: 'translateY(13.5rem) rotate(-16deg)' }"
          class="marquee"
          :pause-on-hover="false"
        >
          <ReviewCard
            v-for="review in firstRow"
            :key="review.username"
            :img="review.img"
            :name="review.name"
            :username="review.username"
            :body="review.body"
          />
        </Marquee>

        <Marquee
          :style="{ transform: 'translateY(26rem) rotate(-16deg)' }"
          reverse
          class="marquee"
          :pause-on-hover="false"
        >
          <ReviewCard
            v-for="review in firstRow"
            :key="review.username"
            :img="review.img"
            :name="review.name"
            :username="review.username"
            :body="review.body"
          />
        </Marquee>
      </div>

      <!-- Gradient overlay to fade to white at the bottom -->
      <div
        class="pointer-events-none absolute inset-0 bg-gradient-to-t from-white to-transparent dark:from-background"
      ></div>
    </div>
  </div>
</template>

<script setup lang="ts">
const isDark = computed(() => useColorMode().value == "dark");

// Reviews data
const reviews = [
  {
    name: "kiri",
    username: "@kiruba_selvi6",
    body: "Oooohhh wowww...!!",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Sébastien Chopin",
    username: "@Atinux",
    body: "You ship 🚢",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Mattia Guariglia",
    username: "@matt_guariglia",
    body: "Omg 🥰",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Nelson🐐",
    username: "@Mathiasokafor3",
    body: "Thank you so much for all you do for the Vue/nuxt eco system.",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Premdas Vm",
    username: "@premdasvm",
    body: "Man, this is soo good! I've been jealous of React because their eco-system had Magic UI and other ones like this. Inspira UI is 🔥🙌🏼",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Pierre",
    username: "@PierreHenryBap",
    body: "It looks really awesome! Just noticed it a couple of days ago and I can’t wait to try it out.",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Waldemar Enns",
    username: "@WaldemarEnns",
    body: "Awesome! ⭐️ed it immediately",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
];

// Rows
const firstRow = ref(reviews);
</script>

<style scoped>
.marquee {
  --duration: 40s;
  position: absolute;
  left: -10%;
  width: 120%;
  padding: 0.5rem 0;
}
</style>

```

### With linear marquee

```vue
<template>
  <div class="flex w-full max-w-7xl flex-col items-center justify-center p-4">
    <div class="flex flex-col gap-4 py-12 text-center">
      <div class="text-3xl font-semibold sm:text-4xl">What people say about us</div>
      <div class="text-lg font-light sm:text-xl">
        Here's what people say about <strong> Inspira UI</strong>
      </div>
    </div>
    <div class="relative flex w-full flex-col items-center justify-center overflow-hidden">
      <!-- First Marquee -->
      <Marquee
        pause-on-hover
        class="[--duration:20s]"
      >
        <ReviewCard
          v-for="review in firstRow"
          :key="review.username"
          :img="review.img"
          :name="review.name"
          :username="review.username"
          :body="review.body"
        />
      </Marquee>

      <!-- Second Marquee (reverse) -->
      <Marquee
        reverse
        pause-on-hover
        class="[--duration:20s]"
      >
        <ReviewCard
          v-for="review in secondRow"
          :key="review.username"
          :img="review.img"
          :name="review.name"
          :username="review.username"
          :body="review.body"
        />
      </Marquee>

      <!-- Left Gradient -->
      <div
        class="pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-white dark:from-background"
      ></div>

      <!-- Right Gradient -->
      <div
        class="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-gradient-to-l from-white dark:from-background"
      ></div>
    </div>
  </div>
</template>

<script setup lang="ts">
const isDark = computed(() => useColorMode().value == "dark");

// Reviews data
const reviews = [
  {
    name: "kiri",
    username: "@kiruba_selvi6",
    body: "Oooohhh wowww...!!",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Sébastien Chopin",
    username: "@Atinux",
    body: "You ship 🚢",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Mattia Guariglia",
    username: "@matt_guariglia",
    body: "Omg 🥰",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Nelson🐐",
    username: "@Mathiasokafor3",
    body: "Thank you so much for all you do for the Vue/nuxt eco system.",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Premdas Vm",
    username: "@premdasvm",
    body: "Man, this is soo good! I've been jealous of React because their eco-system had Magic UI and other ones like this. Inspira UI is 🔥🙌🏼",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Pierre",
    username: "@PierreHenryBap",
    body: "It looks really awesome! Just noticed it a couple of days ago and I can’t wait to try it out.",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
  {
    name: "Waldemar Enns",
    username: "@WaldemarEnns",
    body: "Awesome! ⭐️ed it immediately",
    img: "https://inspira-ui.com/images/x-logo.svg",
  },
];

// Split reviews into two rows
const firstRow = ref(reviews.slice(0, reviews.length / 2));
const secondRow = ref(reviews.slice(reviews.length / 2));
</script>

<style scoped>
.marquee {
  --duration: 40s;
  position: absolute;
  left: -10%;
  width: 120%;
  padding: 0.5rem 0;
}
</style>

```

#### Components used

- [Marquee & Review](https://inspira-ui.com/components/miscellaneous/marquee) component.