my/ui

Command Palette

Search for a command to run...

All components

Anchored Product Tour

tooltips

Origin UI component.

responsive · 480px

Install

Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:

$npx shadcn@latest add https://your-domain/r/comp-389.json

Usage

import Cmp from "@/registry/origin-ui/comp-389";

export default function Demo() {
  return <Cmp />;
}

Component source

"use client";

import {
  ClubIcon,
  DiamondIcon,
  HeartIcon,
  type LucideIcon,
  SpadeIcon,
} from "lucide-react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import {
  Popover,
  PopoverAnchor,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";

interface TourStep {
  icon: LucideIcon;
  title: string;
  description: string;
}

const tourSteps: TourStep[] = [
  {
    description:
      "This is your new workspace. Here you'll find all your projects, recent activities, settings, and more.",
    icon: HeartIcon,
    title: "Heart",
  },
  {
    description:
      "Use the toolbar above to create new projects, invite team members, or access settings.",
    icon: DiamondIcon,
    title: "Diamond",
  },
  {
    description:
      "Click the support icon in the top right corner to access our help center and documentation.",
    icon: ClubIcon,
    title: "Club",
  },
  {
    description:
      "Press ⌘K to open the command palette. Use arrow keys to navigate and Enter to select an action.",
    icon: SpadeIcon,
    title: "Spade",
  },
];

interface CardProps {
  number: number;
  isActive: boolean;
}

function Card({ number, isActive }: CardProps) {
  const content = (
    <div className="flex size-10 items-center justify-center rounded-md bg-secondary font-medium text-muted-foreground text-sm">
      {number + 1}
    </div>
  );

  return isActive ? <PopoverAnchor>{content}</PopoverAnchor> : content;
}

export default function Component() {
  const [currentTip, setCurrentTip] = useState(0);

  const handleNavigation = () => {
    if (currentTip === tourSteps.length - 1) {
      setCurrentTip(0);
    } else {
      setCurrentTip(currentTip + 1);
    }
  };

  return (
    <div className="flex flex-col gap-4">
      <Popover
        onOpenChange={(open) => {
          if (open) setCurrentTip(0);
        }}
      >
        <div className="grid grid-cols-2 place-items-center gap-4">
          {tourSteps.map((step, index) => (
            <Card
              isActive={currentTip === index}
              key={step.title}
              number={index}
            />
          ))}
        </div>

        <PopoverTrigger asChild>
          <Button variant="outline">Start tour</Button>
        </PopoverTrigger>

        <PopoverContent
          className="max-w-[280px] py-3 shadow-none"
          showArrow={true}
          side={currentTip % 2 === 0 ? "left" : "right"}
        >
          <div className="space-y-3">
            <div className="space-y-1">
              <p className="font-medium text-[13px]">
                {tourSteps[currentTip].title}
              </p>
              <p className="text-muted-foreground text-xs">
                {tourSteps[currentTip].description}
              </p>
            </div>
            <div className="flex items-center justify-between gap-2">
              <span className="text-muted-foreground text-xs">
                {currentTip + 1}/{tourSteps.length}
              </span>
              <button
                className="font-medium text-xs hover:underline"
                onClick={handleNavigation}
                type="button"
              >
                {currentTip === tourSteps.length - 1 ? "Start over" : "Next"}
              </button>
            </div>
          </div>
        </PopoverContent>
      </Popover>
    </div>
  );
}

Source: Origin UI