{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "onboarding-dialog",
  "title": "Onboarding Dialog",
  "description": "A multi-step onboarding dialog with carousel navigation.",
  "dependencies": [
    "motion",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "utils",
    "@lumi-ui/button",
    "@lumi-ui/carousel",
    "@lumi-ui/dialog"
  ],
  "files": [
    {
      "path": "registry/components/onboarding-dialog.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport Image from \"next/image\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/ui/button\";\nimport {\n  Carousel,\n  type CarouselApi,\n  CarouselContent,\n  CarouselItem,\n} from \"@/registry/ui/carousel\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/registry/ui/dialog\";\n\nexport function OnboardingDialog() {\n  const [open, setOpen] = React.useState(true);\n  const [carouselApi, setCarouselApi] = React.useState<CarouselApi>();\n  const [activeIndex, setActiveIndex] = React.useState(0);\n\n  React.useEffect(() => {\n    if (!carouselApi) return;\n\n    const onSelect = () => {\n      setActiveIndex(carouselApi.selectedScrollSnap());\n    };\n\n    onSelect();\n    carouselApi.on(\"select\", onSelect);\n    carouselApi.on(\"reInit\", onSelect);\n\n    return () => {\n      carouselApi.off(\"select\", onSelect);\n      carouselApi.off(\"reInit\", onSelect);\n    };\n  }, [carouselApi]);\n\n  const isFirstSlide = activeIndex === 0;\n  const isLastSlide = activeIndex === slides.length - 1;\n  const currentSlide = slides[activeIndex] ?? slides[0];\n\n  const handleNext = () => {\n    if (isLastSlide) {\n      setOpen(false);\n      return;\n    }\n\n    carouselApi?.scrollNext();\n  };\n\n  const handlePrevious = () => {\n    carouselApi?.scrollPrev();\n  };\n\n  const handlePillsKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (!carouselApi) return;\n      const currentIndex = carouselApi.selectedScrollSnap();\n      const lastIndex = slides.length - 1;\n      let targetIndex: number | null = null;\n\n      if (event.key === \"ArrowLeft\") {\n        targetIndex = currentIndex <= 0 ? lastIndex : currentIndex - 1;\n      } else if (event.key === \"ArrowRight\") {\n        targetIndex = currentIndex >= lastIndex ? 0 : currentIndex + 1;\n      } else if (event.key === \"Home\") {\n        targetIndex = 0;\n      } else if (event.key === \"End\") {\n        targetIndex = lastIndex;\n      }\n\n      if (targetIndex === null) {\n        return;\n      }\n\n      event.preventDefault();\n      carouselApi.scrollTo(targetIndex);\n\n      const targetPill = event.currentTarget.querySelector<HTMLButtonElement>(\n        `button[data-pill-index=\"${targetIndex}\"]`,\n      );\n      targetPill?.focus();\n    },\n    [carouselApi],\n  );\n\n  return (\n    <Dialog\n      // disable pointer dismissal so that the user cannot close the dialog by clicking outside of it\n      disablePointerDismissal={true}\n      onOpenChange={setOpen}\n      open={open}\n    >\n      <DialogContent className=\"p-2 sm:p-4\" layout=\"center\">\n        <section>\n          <Carousel\n            className=\"w-full\"\n            opts={{ loop: false }}\n            setApi={setCarouselApi}\n          >\n            <CarouselContent className=\"ml-0 \">\n              {slides.map((slide, index) => (\n                <CarouselItem className=\"pl-0\" key={slide.id}>\n                  <div className=\"p-1\">\n                    <Image\n                      alt={slide.alt}\n                      className=\"aspect-video size-full rounded-lg  w-full object-cover\"\n                      height={720}\n                      priority={index === 0}\n                      sizes=\"(max-width: 640px) 92vw, 40rem\"\n                      src={slide.image}\n                      unoptimized\n                      width={1200}\n                    />\n                  </div>\n                </CarouselItem>\n              ))}\n            </CarouselContent>\n          </Carousel>\n\n          <div\n            className=\"flex items-center justify-center gap-2\"\n            onKeyDownCapture={handlePillsKeyDown}\n          >\n            {slides.map((slide, index) => (\n              <motion.div\n                animate={{\n                  opacity: index === activeIndex ? 1 : 0.8,\n                  width: index === activeIndex ? 24 : 16,\n                }}\n                initial={false}\n                key={slide.id}\n                transition={{ duration: 0.22, ease: \"easeOut\" }}\n              >\n                <Button\n                  aria-current={index === activeIndex ? \"true\" : undefined}\n                  aria-label={`Go to ${slide.title}`}\n                  className={cn(\n                    \"h-2 w-full rounded-full px-0 cursor-pointer\",\n                    index !== activeIndex && \"hover:opacity-100\",\n                  )}\n                  data-pill-index={index}\n                  key={slide.id}\n                  onClick={() => carouselApi?.scrollTo(index)}\n                  size=\"sm\"\n                  variant={index === activeIndex ? \"default\" : \"outline\"}\n                />\n              </motion.div>\n            ))}\n          </div>\n        </section>\n\n        <section className=\"grid p-2\">\n          {slides.map((slide) => {\n            return (\n              <motion.div\n                animate={{\n                  opacity: currentSlide.id === slide.id ? 1 : 0,\n                }}\n                aria-hidden={currentSlide.id !== slide.id}\n                className=\"col-start-1 row-start-1\"\n                initial={false}\n                key={slide.id}\n                style={{\n                  pointerEvents: currentSlide.id === slide.id ? \"auto\" : \"none\",\n                }}\n                transition={{ duration: 0.24, ease: \"easeOut\" }}\n              >\n                <DialogHeader className=\"text-left gap-4\">\n                  <DialogTitle>{slide.title}</DialogTitle>\n                  <DialogDescription>{slide.description}</DialogDescription>\n                </DialogHeader>\n              </motion.div>\n            );\n          })}\n        </section>\n\n        <DialogFooter className=\"p-1 flex-row justify-between sm:justify-between mt-auto\">\n          {!isFirstSlide && (\n            <Button\n              className=\"cursor-pointer\"\n              onClick={handlePrevious}\n              size=\"sm\"\n              variant=\"ghost\"\n            >\n              Back\n            </Button>\n          )}\n          <div className=\"flex items-center gap-2 ml-auto\">\n            <Button\n              className=\"cursor-pointer\"\n              onClick={() => setOpen(false)}\n              size=\"sm\"\n              variant=\"ghost\"\n            >\n              Skip\n            </Button>\n            <Button\n              className=\"cursor-pointer\"\n              disabled={!carouselApi}\n              onClick={handleNext}\n              size=\"sm\"\n            >\n              {isLastSlide ? \"Enter\" : \"Next\"}\n            </Button>\n          </div>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\ntype PlaceholderImageOptions = {\n  title: string;\n  startColor: string;\n  endColor: string;\n  accentColor: string;\n};\n\nconst createPlaceholderImage = ({\n  title,\n  startColor,\n  endColor,\n  accentColor,\n}: PlaceholderImageOptions) => {\n  const svg = `\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"720\" viewBox=\"0 0 1200 720\" fill=\"none\">\n  <defs>\n    <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1200\" y2=\"720\" gradientUnits=\"userSpaceOnUse\">\n      <stop stop-color=\"${startColor}\" />\n      <stop offset=\"1\" stop-color=\"${endColor}\" />\n    </linearGradient>\n  </defs>\n  <rect width=\"1200\" height=\"720\" rx=\"40\" fill=\"url(#bg)\" />\n  <rect x=\"96\" y=\"96\" width=\"1008\" height=\"528\" rx=\"30\" fill=\"${accentColor}\" fill-opacity=\"0.18\" />\n  <circle cx=\"270\" cy=\"220\" r=\"54\" fill=\"${accentColor}\" fill-opacity=\"0.7\" />\n  <rect x=\"352\" y=\"184\" width=\"552\" height=\"72\" rx=\"20\" fill=\"${accentColor}\" fill-opacity=\"0.68\" />\n  <rect x=\"196\" y=\"350\" width=\"404\" height=\"36\" rx=\"18\" fill=\"${accentColor}\" fill-opacity=\"0.62\" />\n  <rect x=\"196\" y=\"410\" width=\"620\" height=\"30\" rx=\"15\" fill=\"${accentColor}\" fill-opacity=\"0.56\" />\n  <rect x=\"196\" y=\"456\" width=\"720\" height=\"30\" rx=\"15\" fill=\"${accentColor}\" fill-opacity=\"0.46\" />\n  <text x=\"196\" y=\"565\" fill=\"${accentColor}\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"46\" font-weight=\"700\">${title}</text>\n</svg>`;\n\n  return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;\n};\n\nconst slides = [\n  {\n    alt: \"Dashboard preview\",\n    description:\n      \"Track tasks, documents, and progress from one focused dashboard built for daily execution. Track tasks, documents, and progress from one focused dashboard built for daily execution.\",\n    id: \"welcome\",\n    image: createPlaceholderImage({\n      accentColor: \"#0B1E47\",\n      endColor: \"#CDE2FF\",\n      startColor: \"#EAF2FF\",\n      title: \"Welcome Dashboard\",\n    }),\n    title: \"Welcome to your new workspace\",\n  },\n  {\n    alt: \"Automation workflow preview\",\n    description:\n      \"Use smart flows to remove manual busywork and keep your team aligned without extra status meetings.\",\n    id: \"automations\",\n    image: createPlaceholderImage({\n      accentColor: \"#0A3D30\",\n      endColor: \"#CAF6E8\",\n      startColor: \"#E8FFF7\",\n      title: \"Automations\",\n    }),\n    title: \"Automate repetitive work\",\n  },\n  {\n    alt: \"Collaboration preview\",\n    description:\n      \"Share feedback directly where decisions happen so updates stay clear, timely, and easy to follow.\",\n    id: \"collaboration\",\n    image: createPlaceholderImage({\n      accentColor: \"#4A2B00\",\n      endColor: \"#FFE8C2\",\n      startColor: \"#FFF6E8\",\n      title: \"Team Collaboration\",\n    }),\n    title: \"Collaborate in context\",\n  },\n  {\n    alt: \"Insights reporting preview\",\n    description:\n      \"Turn activity into insights with reporting views that highlight what is improving and what needs attention. Turn activity into insights with reporting views that highlight what is improving and what needs attention.Turn activity into insights with reporting views that highlight what is improving and what needs attention.\",\n    id: \"insights\",\n    image: createPlaceholderImage({\n      accentColor: \"#2D1457\",\n      endColor: \"#E1D4FF\",\n      startColor: \"#F2ECFF\",\n      title: \"Insights Reporting\",\n    }),\n    title: \"Measure outcomes\",\n  },\n] as const;\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "overlays-dialogs",
    "dashboard"
  ],
  "type": "registry:component"
}