[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\nCheckpoint.tsx"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"tailwindCSS.experimental.classRegex\": [\n    [\"cva\\\\(([^)]*)\\\\)\", \"[\\\"'`]([^\\\"'`]*).*?[\\\"'`]\"],\n    [\"cx\\\\(([^)]*)\\\\)\", \"(?:'|\\\"|`)([^']*)(?:'|\\\"|`)\"]\n  ],\n  \"editor.fontLigatures\": true,\n  \"editor.renderWhitespace\": \"none\",\n\n  \"tailwindCSS.includeLanguages\": {\n    \"html\": \"html\",\n    \"javascript\": \"javascript\",\n    \"typescript\": \"typescript\",\n    \"css\": \"css\"\n  },\n  \"editor.quickSuggestions\": {\n    \"strings\": true\n  },\n  \"typescript.tsserver.experimental.enableProjectDiagnostics\": true\n}\n"
  },
  {
    "path": "README.md",
    "content": "## Prismify\n\nPrismify is a web app that aims to revitalize & enhance boring images/screenshots. With prismify, you can effortlessly enhance your images/screenshots.\n\n![Prismify](https://github.com/Sls0n/Prismify/assets/102340248/d37df848-59da-4e26-8dbc-451562ef6c55)\n\n\n## Preview\n![image](https://github.com/Sls0n/Prismify/assets/102340248/5e004ca7-d53a-400e-993c-a34cd9bdc829)\n![prismify_ss](https://github.com/Sls0n/Prismify/assets/102340248/33323217-59ba-48a1-a494-09fa8658f354)\n\n\n## Tech Stacks\n\n- Typescript\n- Next\n- React\n- Tailwind\n- Prisma\n- Zustand\n\n## Perfect lighthouse score\n![prismify-render-1704521997749](https://github.com/Sls0n/Prismify/assets/102340248/1f268d3e-cd9b-4d88-88da-247607ccbc45)\n\n## Setup\nRun the following command to install dependencies and generate the Prisma client:\n\n```bash\npnpm run setup\n```\n\n\n\n"
  },
  {
    "path": "app/(routes)/about/page.tsx",
    "content": "/* eslint-disable react/no-unescaped-entities */\nimport React from 'react'\nimport BackButton from '@/components/ui/back-button'\nimport { Text } from '@/components/ui/text'\nimport type { Metadata } from 'next'\nimport Image from 'next/image'\nimport { GradientText } from '@/components/ui/gradient-text'\n\nexport const metadata: Metadata = {\n  title: 'About - Prismify',\n  description: 'Read details about Prismify.',\n  openGraph: {\n    title: 'About - Prismify',\n    description: 'Read details about Prismify.',\n  },\n  alternates: {\n    canonical: 'https://prismify.vercel.app/about',\n  },\n}\n\nexport default function page() {\n  return (\n    <section className=\"container flex w-full flex-col gap-8 pt-[72px] lg:gap-16\">\n      <div className=\"mt-8\">\n        <BackButton />\n      </div>\n\n      <article className=\"mx-auto h-fit w-full max-w-prose rounded-md\">\n        <div className=\"mb-16 flex w-full items-start justify-between\">\n          <div className=\"mx-auto flex flex-col gap-3\">\n            {/* Title */}\n            <GradientText\n              as=\"h1\"\n              variant=\"purple\"\n              className=\"line-clamp-2 border-b-[3px] border-[#898aeb] text-center text-[2rem] font-semibold capitalize leading-tight md:text-[2.8rem]\"\n            >\n              <span className=\"!font-medium\">About</span> &mdash; Prismify\n            </GradientText>\n          </div>\n        </div>\n\n        <div className=\"prose prose-lg prose-neutral mb-16 dark:prose-invert prose-p:tracking-[0.002em] prose-p:text-dark prose-img:rounded-lg\">\n          <figure className=\"\">\n            <Image\n              width={1280}\n              height={720}\n              src=\"https://prismify.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F5262c091-eb0d-485e-8fdc-7a16b54935b4%2F01b77d12-90a7-42a9-87b3-d43ebb33aca3%2Fprofile-4TcKvlK3c7c8cg1PP9gwBEJrYaY2-128shots_so..png?table=block&id=17227f20-3d4b-4256-a2a7-54f8949a03f7&spaceId=5262c091-eb0d-485e-8fdc-7a16b54935b4&width=2000&userId=&cache=v2\"\n              alt=\"About Prismify\"\n              className=\"aspect-auto max-w-full object-cover\"\n            />\n          </figure>\n          <p>\n            Hey there, fellow entrepreneurs! Ever felt like banging your head\n            against the wall trying to create eye-catching designs for your\n            product or social media? I hear you! But guess what? Prismify's here\n            to save the day.\n          </p>{' '}\n          <h3 className=\"text-dark\">\n            Let's dive into the struggles we all face:\n          </h3>\n          <ul>\n            <li>\n              <strong>Zero Design Skills:</strong> Who has time to become a\n              design pro? With Prismify, you don't need to! It's like having a\n              magic wand for your visuals.\n            </li>\n            <li>\n              <strong>Clock's Ticking:</strong> Time is money, they say.\n              Prismify respects that – swift, efficient, and no-nonsense design\n              tools.\n            </li>\n            <li>\n              <strong>Quality Matters:</strong> We all want our stuff to look\n              top-notch. Prismify keeps your visuals sharp and professional\n              without the designer price tag.\n            </li>\n          </ul>\n          <p>\n            Now, hold on to your hats! Here's how Prismify makes design a\n            cakewalk:\n          </p>\n          <ul>\n            <li>\n              Browser Frame Mockups: Pop your product into a fancy browser frame\n              in a snap!\n            </li>\n            <li>\n              Custom Gradients: Gradient backgrounds that'll make your graphics\n              pop like fireworks! With also adaptive gradients support!\n            </li>\n            <li>\n              Text Tricks & Notes: Jazz up your visuals with text enhancements\n              and handy annotations.\n            </li>\n            <li>\n              Color Magic & 3D Vibes: Prismify's color game and 3D effects?\n              Mind-blowing.\n            </li>\n            <li>\n              Hi-Res & Lightning-Speed Edits: Get crisp, high-res images without\n              the wait. Lightning-fast editing? You got it!\n            </li>\n          </ul>\n          <p>Perks for You:</p>\n          <ul>\n            <li>\n              Time Saver: Spend less time stressing over designs, more time\n              making things happen.\n            </li>\n            <li>\n              Pro-Level Designs: No more 'amateur hour' visuals. Prismify gives\n              you that polished, pro look.\n            </li>\n            <li>\n              Versatility Rules: Social media, websites, product shots –\n              Prismify's got your back for any visual need.\n            </li>\n          </ul>\n          <p>Feeling the design itch? Scratch it with Prismify!</p>\n          <blockquote>\n            \"Transform Your Visuals Today! Try Prismify – Your Visual Upgrade\n            Shortcut.\"\n          </blockquote>\n          <p>\n            Wrapping it Up: Prismify's not just a tool; it's your secret weapon\n            for killer SaaS visuals and social media. Remember to show, not just\n            tell – demos and examples make all the difference. Get ready to rock\n            those visuals!\n          </p>\n        </div>\n      </article>\n    </section>\n  )\n}\n"
  },
  {
    "path": "app/(routes)/articles/[slug]/loading.tsx",
    "content": "import Loader from '@/components/loader'\n\nexport default function Loading() {\n  return <Loader />\n}\n"
  },
  {
    "path": "app/(routes)/articles/[slug]/page.tsx",
    "content": "import BackButton from '@/components/ui/back-button'\nimport { Badge } from '@/components/ui/badge'\nimport { Text } from '@/components/ui/text'\nimport prismadb from '@/libs/prismadb'\nimport {\n  generateBadgeVariant,\n  generateFormattedBlogDate,\n  separateCommas,\n} from '@/utils/helper-fns'\nimport { Calendar } from 'lucide-react'\nimport type { Metadata } from 'next'\nimport Image from 'next/image'\nimport { notFound } from 'next/navigation'\nimport { cache } from 'react'\n\ntype ArticleProps = {\n  params: Promise<{\n    slug: string\n  }>\n}\n\nexport const revalidate = 3600 // 1 hour\n\nexport async function generateStaticParams() {\n  const blogs = await prismadb.article.findMany({\n    select: {\n      slug: true,\n    },\n  })\n\n  return blogs.map((blog) => ({\n    slug: blog.slug,\n  }))\n}\n\nconst getBlog = cache(async (slug: string) => {\n  const blog = await prismadb.article.findFirst({\n    where: {\n      slug,\n    },\n  })\n\n  return blog\n})\n\nexport async function generateMetadata(props: ArticleProps) {\n  const params = await props.params;\n  const blog = await getBlog(params.slug)\n\n  if (!blog) {\n    return\n  }\n\n  const metadata: Metadata = {\n    title: `${blog.title} - Prismify`,\n    description: blog.summary,\n    openGraph: {\n      title: `${blog.title} - Prismify`,\n      description: blog?.summary || 'N/A',\n      locale: 'en_US',\n      url: `https://prismify.vercel.app/articles/${blog.slug}`,\n      type: 'article',\n      images: [\n        {\n          url:\n            blog.imageUrl ?? 'https://prismify.vercel.app/opengraph-image.jpg',\n          width: 1280,\n          height: 720,\n          alt: blog.title,\n        },\n      ],\n    },\n    twitter: {\n      creator: '@xSls0n_007',\n      title: `${blog.title} - Prismify`,\n      description: blog.summary || 'N/A',\n      card: 'summary_large_image',\n      images: [\n        {\n          url:\n            blog.imageUrl ?? 'https://prismify.vercel.app/opengraph-image.jpg',\n          width: 1280,\n          height: 720,\n          alt: blog.title,\n        },\n      ],\n    },\n    alternates: {\n      canonical: `https://prismify.vercel.app/articles/${blog.slug}`,\n    },\n    publisher: 'Prismify',\n  }\n\n  return metadata\n}\n\nexport default async function ArticlePage(props: ArticleProps) {\n  const params = await props.params;\n  const blog = await getBlog(params.slug)\n\n  if (!blog) {\n    return notFound()\n  }\n\n  return (\n    <section className=\"container flex w-full flex-col gap-8 pt-[72px] lg:gap-16\">\n      <div className=\"mt-8\">\n        <BackButton />\n      </div>\n\n      <article className=\"mx-auto h-fit w-full max-w-prose rounded-md\">\n        <div className=\"mb-16 flex w-full items-start justify-between\">\n          <div className=\"flex flex-col gap-3\">\n            {/* Time published/updated */}\n            <div className=\"flex items-center\">\n              <Calendar className=\"mr-2 h-[1.15rem] w-[1.15rem] text-white/40\" />\n              <time\n                className=\"text-base font-normal text-dark/60\"\n                dateTime={\n                  blog?.updatedAt.toISOString() ?? blog?.createdAt.toISOString()\n                }\n              >\n                {generateFormattedBlogDate(blog?.createdAt, blog?.updatedAt)}\n              </time>\n            </div>\n\n            {/* Title */}\n            <Text\n              variant=\"h1\"\n              bold\n              className=\"text-[2rem] font-bold capitalize leading-tight text-dark md:text-[2.8rem]\"\n            >\n              {blog.title ?? 'N/A'}\n            </Text>\n            {/* Category */}\n            {blog?.category && (\n              <div className=\"flex items-center gap-2\">\n                {separateCommas(blog?.category)!.map((category) => (\n                  <Badge\n                    key={category}\n                    variant={generateBadgeVariant(category)}\n                  >\n                    {category}\n                  </Badge>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n\n        <div className=\"prose prose-lg prose-neutral mb-16 dark:prose-invert prose-p:tracking-[0.002em] prose-p:text-dark prose-a:cursor-pointer prose-a:text-purple\">\n          {blog?.imageUrl && (\n            <figure className=\"h-fit overflow-hidden rounded-lg bg-formDark\">\n              <Image\n                src={blog?.imageUrl ?? '/images/fallback.jpg'}\n                alt={'cover image'}\n                width={1280}\n                height={720}\n                priority\n                className=\"aspect-auto max-w-full object-cover\"\n              />\n\n              {/* <figcaption>\n              Picture\n            </figcaption> */}\n            </figure>\n          )}\n          {blog?.content && (\n            <div\n              dangerouslySetInnerHTML={{\n                __html: blog.content,\n              }}\n            ></div>\n          )}\n        </div>\n      </article>\n    </section>\n  )\n}\n"
  },
  {
    "path": "app/(routes)/articles/page.tsx",
    "content": "import prismadb from '@/libs/prismadb'\nimport { Text } from '@/components/ui/text'\nimport ArticleCard from '@/components/articles/article-card'\nimport { formatDate, separateCommas } from '@/utils/helper-fns'\nimport type { Metadata } from 'next'\nimport { unstable_cache as cache } from 'next/cache'\n\nexport const metadata: Metadata = {\n  title: 'Articles - Prismify',\n  description: 'Read latest articles from Prismify.',\n  openGraph: {\n    title: 'Articles - Prismify',\n    description: 'Read latest articles from Prismify.',\n    type: 'article',\n    url: 'https://prismify.vercel.app/articles',\n  },\n  alternates: {\n    canonical: 'https://prismify.vercel.app/articles',\n  },\n}\n\nconst getCachedArticles = cache(\n  async () => {\n    return await prismadb.article.findMany({\n      orderBy: {\n        createdAt: 'desc',\n      },\n    })\n  },\n  ['articles'],\n  {\n    revalidate: 60 * 60, // 1 hour\n    tags: ['articles'],\n  }\n)\n\nexport default async function Article() {\n  const articles = await getCachedArticles()\n\n  return (\n    <section className=\"w-full flex-1 pt-[72px]\">\n      <div className=\"mb-16 flex h-48 w-full flex-col items-center justify-center bg-black/10\">\n        <Text\n          variant=\"h1\"\n          bold\n          className=\"mb-2 line-clamp-2 text-[2rem] font-bold capitalize leading-tight tracking-normal text-dark md:text-[2.8rem]\"\n        >\n          Articles\n        </Text>\n\n        <Text\n          variant=\"bodyMedium\"\n          className=\"line-clamp-2 flex items-center gap-1 text-dark/70 \"\n        >\n          <span>Read latest articles from</span>\n          <span className=\"inline-flex w-fit items-center gap-1 rounded-md bg-indigo-500/10 px-2 py-1 text-xs font-medium text-purple shadow-sm ring-1 ring-inset ring-indigo-500/20 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\">\n            Prismify\n          </span>\n        </Text>\n      </div>\n\n      <ul\n        className=\"container mt-5 grid grid-cols-1 grid-rows-2 gap-16 sm:mt-10 sm:grid-cols-2 md:mt-24 lg:grid-cols-3\"\n        role=\"list\"\n      >\n        {articles.length === 0 ? (\n          <div className=\"mb-24 text-center\">\n            <Text variant=\"bodyMedium\" className=\"text-dark/70\">\n              &mdash; The list is empty. Go back to previous page\n            </Text>\n          </div>\n        ) : (\n          articles.map((article) => {\n            return (\n              <div key={article.id} className=\"relative col-span-1 row-span-1\">\n                <ArticleCard\n                  title={article.title ?? 'N/A'}\n                  image={article.imageUrl ?? '/images/fallback.jpg'}\n                  date={formatDate(article.updatedAt) ?? 'N/A'}\n                  href={`/articles/${article.slug}`}\n                  category={\n                    separateCommas(article?.category ?? 'Design')?.at(0) ?? ''\n                  }\n                />\n              </div>\n            )\n          })\n        )}\n      </ul>\n    </section>\n  )\n}\n"
  },
  {
    "path": "app/(routes)/articles/sitemap.ts",
    "content": "import prismadb from '@/libs/prismadb'\nimport { MetadataRoute } from 'next'\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n  const response = await prismadb.article.findMany({\n    select: {\n      slug: true,\n      updatedAt: true,\n    },\n  })\n\n  const sitemapUrls: MetadataRoute.Sitemap = response?.map((p) => {\n    return {\n      url: `https://prismify.vercel.app/articles/${p.slug}`,\n      lastModified: new Date(p.updatedAt),\n      changeFrequency: 'monthly',\n    }\n  })\n\n  return sitemapUrls\n}\n"
  },
  {
    "path": "app/(routes)/layout.tsx",
    "content": "import Footer from '@/components/footer'\nimport React from 'react'\n\nexport default function AdminLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return (\n    <main className=\"flex h-full w-full flex-col\">\n      {children}\n\n      <Footer />\n    </main>\n  )\n}\n"
  },
  {
    "path": "app/admin/layout.tsx",
    "content": "import React from 'react'\n\nexport default function AdminLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return (\n    <main className=\"container h-full w-full\">\n      {children}\n    </main>\n  )\n}\n"
  },
  {
    "path": "app/admin/write-article/blog-form.tsx",
    "content": "'use client'\n\nimport { EditorProvider, useCurrentEditor } from '@tiptap/react'\nimport {\n  Bold,\n  Code,\n  Heading,\n  ImagePlus,\n  Italic,\n  Link,\n  List,\n  ListOrdered,\n  Quote,\n  Space,\n  Strikethrough,\n  Underline,\n} from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\n// import { FileUpload } from '@/components/file-upload'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { cn } from '@/utils/button-utils'\nimport { extensions } from '@/utils/tiptap-extensions'\nimport { useMutation } from '@tanstack/react-query'\nimport { Text } from '@/components/ui/text'\nimport { useTiptap } from '@/store/use-tiptap'\nimport axios, { AxiosError } from 'axios'\nimport { toast } from '@/hooks/use-toast'\nimport { Textarea } from '@/components/ui/text-area'\n\nexport default function BlogForm() {\n  const [blogOutput, setBlogOutput] = useState<string>('')\n  const [title, setTitle] = useState<string>('')\n  const [summary, setSummary] = useState<string>('')\n  const [category, setCategory] = useState<string>('')\n  const [slug, setSlug] = useState<string>('')\n  const [mainBlogImg, setMainBlogImg] = useState<string>('')\n\n  const { mutate: publishBlog, isLoading: isPublishing } = useMutation({\n    mutationFn: async () => {\n      const res = await axios.post('/api/article/post', {\n        title,\n        summary,\n        category,\n        slug,\n        imageUrl: mainBlogImg,\n        content: blogOutput,\n      })\n\n      return res\n    },\n    onSuccess: () => {\n      toast({\n        title: 'Blog published successfully',\n      })\n    },\n    onError: (err) => {\n      if (err instanceof AxiosError) {\n        if (err.response?.status === 400) {\n          toast({\n            title: err?.response?.data?.[0]?.message,\n            variant: 'destructive',\n          })\n        }\n\n        if (err.response?.status === 401) {\n          toast({\n            title: 'You are not authorized to publish this blog',\n            description: 'Please login to publish this blog',\n            variant: 'destructive',\n          })\n        }\n\n        if (err.response?.status === 409) {\n          toast({\n            title: 'Blog with this slug already exists',\n            description: 'Please modify the slug a bit.',\n            variant: 'destructive',\n          })\n        }\n\n        if (err.response?.status === 500) {\n          toast({\n            title: 'Something went wrong',\n            description: 'Please try again later',\n            variant: 'destructive',\n          })\n        }\n      }\n      console.log(err)\n    },\n  })\n\n  const publishBlogHandler = () => {\n    publishBlog()\n  }\n\n  const slugifyTitle = async (title: string) => {\n    const slugify = (await import('slugify')).default\n    setSlug(slugify(title, { lower: true, strict: true }))\n  }\n\n  return (\n    <>\n      <div className=\"mb-8 mt-8 space-y-6 rounded-lg border-[1.5px] border-border bg-[#151515] p-8\">\n        <div className=\"space-y-2\">\n          <Text className=\"text-dark/90\" variant=\"bodyLarge\" semibold>\n            Title\n          </Text>\n\n          <Input\n            placeholder=\"Write title for your blog...\"\n            className=\"h-16 border-transparent bg-[#151515] p-0 placeholder:text-dark/50 focus-visible:ring-0 focus-visible:ring-transparent md:text-xl \"\n            value={title}\n            onChange={(e) => {\n              setTitle(e.target.value)\n              slugifyTitle(e.target.value)\n            }}\n            required\n          />\n        </div>\n\n        <div className=\"space-y-2\">\n          <Text className=\"text-dark/90\" variant=\"bodyLarge\" semibold>\n            Summary\n          </Text>\n\n          <Textarea\n            placeholder=\"Summary of the blog...\"\n            className=\"bg-transparent h-32 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl\"\n            value={summary}\n            onChange={(e) => setSummary(e.target.value)}\n          />\n        </div>\n\n        <div className=\"space-y-2\">\n          <Text className=\"text-dark/90\" variant=\"bodyLarge\" semibold>\n            Category (Comma separated)\n          </Text>\n\n          <Input\n            placeholder=\"Eg: Design, Marketing\"\n            className=\"h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl\"\n            value={category}\n            onChange={(e) => setCategory(e.target.value)}\n          />\n        </div>\n\n        <div className=\"space-y-2\">\n          <Text className=\"text-dark/90\" variant=\"bodyLarge\" semibold>\n            Slug\n          </Text>\n\n          <Input\n            placeholder=\"Eg: blog-title-slug\"\n            className=\"h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl\"\n            value={slug}\n            onChange={(e) => setSlug(e.target.value)}\n          />\n        </div>\n\n        <div className=\"space-y-2\">\n          <Text className=\"text-dark/90\" variant=\"bodyLarge\" semibold>\n            Image URL\n          </Text>\n\n          <Input\n            placeholder=\"Eg: https://images.unsplash.com/photo/...\"\n            className=\"h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl\"\n            value={mainBlogImg}\n            onChange={(e) => setMainBlogImg(e.target.value)}\n          />\n        </div>\n      </div>\n\n      <div className=\"mb-8 mt-4 w-full space-y-6 rounded-lg border-[1.5px] border-border bg-[#151515] p-8\">\n        <div className=\"w-full \">\n          <Text variant=\"bodyXLarge\" className=\"pb-4 text-dark/90\" semibold>\n            Write your blog here\n          </Text>\n\n          <hr className=\"-mx-8 border-border pb-4\" />\n\n          <EditorProvider\n            slotBefore={<MenuBar />}\n            extensions={extensions}\n            parseOptions={{\n              preserveWhitespace: true,\n            }}\n            editorProps={{\n              attributes: {\n                class:\n                  'prose prose-md max-w-prose mx-auto prose-neutral mb-16 md:prose-lg prose-img:rounded-md focus:outline-none min-h-[30rem] dark:prose-invert',\n              },\n            }}\n            onUpdate={(content) =>\n              setBlogOutput(content?.editor?.getHTML())\n            }\n          />\n        </div>\n      </div>\n\n      <Button\n        isLoading={isPublishing}\n        onClick={publishBlogHandler}\n        variant=\"default\"\n        className=\"mb-24 max-w-[10rem]\"\n      >\n        <Text variant=\"bodyMedium\" semibold>\n          Publish blog\n        </Text>\n      </Button>\n    </>\n  )\n}\n\nconst MenuBar = () => {\n  const { editor } = useCurrentEditor()\n\n  const addImage = useCallback(() => {\n    const url = window.prompt('URL')\n\n    if (url) {\n      editor?.chain().focus().setImage({ src: url }).run()\n    }\n  }, [editor])\n\n  const setLink = useCallback(() => {\n    const previousUrl = editor?.getAttributes('link').href as string\n    const url = window.prompt('URL', previousUrl)\n\n    // cancelled\n    if (url === null) {\n      return\n    }\n\n    // empty\n    if (url === '') {\n      editor?.chain().focus().extendMarkRange('link').unsetLink().run()\n\n      return\n    }\n\n    // update link\n    editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()\n  }, [editor])\n\n  return (\n    <div className=\"sticky top-[4.5rem] z-50 bg-[#151515]\">\n      <div className=\"z-50 flex flex-wrap gap-y-4 bg-[#151515] pb-5 pt-2 backdrop-blur-md\">\n        <Button\n          variant=\"menuItem\"\n          onClick={() =>\n            editor?.chain().focus().toggleHeading({ level: 3 }).run()\n          }\n          disabled={\n            !editor?.can().chain().focus().toggleHeading({ level: 3 }).run()\n          }\n          className={cn(\n            editor?.isActive('heading', { level: 3 })\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : 'border border-transparent',\n            'mr-2 px-2.5 text-foreground/80 '\n          )}\n        >\n          <Heading className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleBold().run()}\n          disabled={!editor?.can().chain().focus().toggleBold().run()}\n          className={cn(\n            editor?.isActive('bold')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : 'border border-transparent',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <Bold className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleItalic().run()}\n          disabled={!editor?.can().chain().focus().toggleItalic().run()}\n          className={cn(\n            editor?.isActive('italic')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : 'border border-transparent',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <Italic className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleUnderline().run()}\n          disabled={!editor?.can().chain().focus().toggleUnderline().run()}\n          className={cn(\n            editor?.isActive('underline')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : 'border border-transparent',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <Underline className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleStrike().run()}\n          disabled={!editor?.can().chain().focus().toggleStrike().run()}\n          className={cn(\n            editor?.isActive('strike')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : 'border border-transparent',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <Strikethrough className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleCodeBlock().run()}\n          disabled={!editor?.can().chain().focus().toggleCodeBlock().run()}\n          className={cn(\n            editor?.isActive('codeBlock')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : 'border border-transparent',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <Code className=\"h-5 w-5\" />\n        </Button>\n\n        <div className=\"my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10\"></div>\n\n        <Button\n          className=\"mr-2 px-2.5 text-foreground/80\"\n          variant=\"menuItem\"\n          onClick={addImage}\n        >\n          <ImagePlus className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => {\n            editor?.isActive('link')\n              ? editor?.chain().focus().unsetLink().run()\n              : setLink()\n          }}\n          className={cn(\n            editor?.isActive('link')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : '',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <Link className=\"h-5 w-5\" />\n        </Button>\n\n        <div className=\"my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10\"></div>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleBulletList().run()}\n          disabled={!editor?.can().chain().focus().toggleBulletList().run()}\n          className={cn(\n            editor?.isActive('bulletList')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : '',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <List className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleOrderedList().run()}\n          disabled={!editor?.can().chain().focus().toggleOrderedList().run()}\n          className={cn(\n            editor?.isActive('orderedList')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : '',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <ListOrdered className=\"h-5 w-5\" />\n        </Button>\n\n        <div className=\"my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10\"></div>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().toggleBlockquote().run()}\n          disabled={!editor?.can().chain().focus().toggleBlockquote().run()}\n          className={cn(\n            editor?.isActive('blockquote')\n              ? 'border border-indigo-400/10 bg-indigo-400/10'\n              : '',\n            'mr-2 px-2.5 text-foreground/80'\n          )}\n        >\n          <Quote className=\"h-5 w-5\" />\n        </Button>\n\n        <Button\n          variant=\"menuItem\"\n          onClick={() => editor?.chain().focus().setHorizontalRule().run()}\n          className={cn('mr-2 px-2.5 text-foreground/80')}\n        >\n          <Space className=\"h-5 w-5\" />\n        </Button>\n      </div>\n      <hr className=\"-mx-8 border-border pb-10\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/admin/write-article/page.tsx",
    "content": "import Loader from '@/components/loader'\nimport BackButton from '@/components/ui/back-button'\nimport dynamic from 'next/dynamic'\nimport type { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Admin - Write Article',\n  description: 'Write an article for the blog.',\n}\n\nconst BlogForm = dynamic(() => import('./blog-form'), {\n  loading: () => <Loader />,\n})\n\nexport default function page() {\n  return (\n    <section className=\"mx-auto mt-6 h-[100dvh] w-full max-w-prose pt-[72px] lg:mt-8\">\n      <BackButton />\n      <BlogForm />\n    </section>\n  )\n}\n"
  },
  {
    "path": "app/api/article/post/route.ts",
    "content": "import { z } from 'zod'\nimport { NextResponse } from 'next/server'\nimport prismadb from '@/libs/prismadb'\nimport { getCurrentSession } from '@/utils/auth-options'\nimport { postSchema } from '@/libs/validators/article-post-validator'\nimport { revalidateTag } from 'next/cache'\n\nexport async function POST(request: Request) {\n  try {\n    const body = await request.json()\n    const { title, summary, category, slug, content, imageUrl } =\n      postSchema.parse(body)\n\n    const session = await getCurrentSession()\n\n    if (!session || !session.user.isCreator) {\n      return new NextResponse('Unauthorized', { status: 401 })\n    }\n\n    const slugExits = await prismadb.article.findFirst({\n      where: { slug },\n    })\n\n    if (slugExits) {\n      return new NextResponse('Article already exists', { status: 409 })\n    }\n\n    const blog = await prismadb.article.create({\n      data: {\n        title,\n        summary,\n        category,\n        slug,\n        content,\n        imageUrl,\n      },\n    })\n\n    revalidateTag('articles')\n\n    return new NextResponse('OK')\n  } catch (error) {\n    console.log(error)\n    console.log(error)\n    if (error instanceof z.ZodError) {\n      return new NextResponse(error.message, { status: 400 })\n    }\n    return new NextResponse('Something went wrong', { status: 500 })\n  }\n}\n"
  },
  {
    "path": "app/api/auth/[...nextauth]/route.ts",
    "content": "import { AuthOptions } from 'next-auth'\nimport { authOptions } from '@/utils/auth-options'\nimport NextAuth from 'next-auth'\n\nconst handler = NextAuth(authOptions)\nexport { handler as GET, handler as POST }\n"
  },
  {
    "path": "app/api/user/settings/route.ts",
    "content": "import { z } from 'zod'\nimport { NextResponse } from 'next/server'\nimport prismadb from '@/libs/prismadb'\nimport { getCurrentSession } from '@/utils/auth-options'\nimport { userSettingsSchema } from '@/libs/validators/user-settings-validator'\n\nexport async function GET() {\n  try {\n    const session = await getCurrentSession()\n    if (!session) {\n      return new NextResponse('Unauthorized', { status: 401 })\n    }\n\n    const dbUser = await prismadb.user.findUnique({\n      where: { id: session.user.id },\n      select: {\n        id: true,\n        name: true,\n        email: true,\n        image: true,\n        isCreator: true,\n        createdAt: true,\n      },\n    })\n\n    return NextResponse.json({\n      session: {\n        id: session.user.id,\n        name: session.user.name,\n        email: session.user.email,\n        image: session.user.image,\n        isCreator: session.user.isCreator,\n      },\n      database: dbUser,\n    })\n  } catch (error) {\n    return new NextResponse('Something went wrong', { status: 500 })\n  }\n}\n\nexport async function PATCH(request: Request) {\n  try {\n    const session = await getCurrentSession()\n    if (!session) {\n      return new NextResponse('Unauthorized', { status: 401 })\n    }\n\n    const body = await request.json()\n    const { name, image } = userSettingsSchema.parse(body)\n\n    const updateData = {\n      name: name ?? undefined,\n      image: image ?? undefined,\n    }\n\n    await prismadb.user.update({\n      where: { id: session.user.id },\n      data: updateData,\n    })\n\n    return new NextResponse('OK')\n  } catch (error) {\n    if (error instanceof z.ZodError) {\n      return new NextResponse(error.message, { status: 400 })\n    }\n    return new NextResponse('Something went wrong', { status: 500 })\n  }\n}\n"
  },
  {
    "path": "app/error.tsx",
    "content": "'use client'\n\n/* eslint-disable react/no-unescaped-entities */\nimport { Button, buttonVariants } from '@/components/ui/button'\nimport { cn } from '@/utils/button-utils'\nimport { MailWarning, RotateCcw } from 'lucide-react'\nimport Link from 'next/link'\nimport { useEffect } from 'react'\n\nexport default function Error({\n  error,\n  reset,\n}: {\n  error: Error & { digest?: string }\n  reset: () => void\n}) {\n  useEffect(() => {\n    console.error(error)\n  }, [error])\n\n  return (\n    <div className=\"flex flex-col items-center px-4 pt-40 text-center sm:px-6 lg:px-8\">\n      <h1 className=\"block bg-gradient-to-br from-[#898AEB] via-[#898dd9]/80 to-[#8e8ece] bg-clip-text text-center text-4xl font-bold text-transparent sm:text-6xl \">\n        Something went amiss.\n      </h1>\n      <br />\n      <p className=\"max-w-xl text-center text-lg font-medium text-foreground opacity-90 md:text-xl\">\n        <strong className=\"font-semibold\">Sorry!</strong>{' '}\n        {error.message ? error.message : 'Looks like prismify needs glasses :('}\n      </p>\n\n      <div className=\"mt-10 flex flex-col items-center justify-center gap-2 sm:flex-row sm:gap-3\">\n        <Button onClick={reset} className=\"w-48 text-base sm:w-fit\" variant=\"stylish\">\n          <RotateCcw className=\"mr-2 h-5 w-5 text-foreground/80\" />\n          Pray & Retry\n        </Button>\n        <Link\n          href=\"mailto:silson0072@gmail.com\"\n          className={cn(\n            buttonVariants({ variant: 'default' }),\n            'w-48 px-4 text-base sm:w-fit'\n          )}\n        >\n          <MailWarning className=\"mr-2 h-5 w-5 text-foreground/80\" />\n          Report\n        </Link>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import ClarityScript from '@/components/clarity-script'\nimport Navbar from '@/components/navbar'\nimport { Toaster } from '@/components/ui/toaster'\nimport Providers from '@/providers'\nimport '@/styles/globals.css'\nimport { cn } from '@/utils/button-utils'\nimport { Analytics } from '@vercel/analytics/react'\nimport type { Metadata, Viewport } from 'next'\nimport { Plus_Jakarta_Sans } from 'next/font/google'\n\nconst Font = Plus_Jakarta_Sans({ subsets: ['latin'] })\n\nexport const metadata: Metadata = {\n  title:\n    'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',\n  description:\n    'Easily make your SaaS/product shots and graphics design stand out. Create beautiful screenshots and graphics for websites, social media, and more. With Prismify, you get browser frames, gradient backgrounds, text, annotations.',\n  verification: {\n    google: 'cum1ckoCozAtSA3Xcn4UX_xR_FlfrlzKzQRa7nYQ2YM',\n  },\n  icons: {\n    icon: '/favicons/favicon.ico',\n    apple: '/favicons/apple-touch-icon.png',\n  },\n  applicationName: 'Prismify',\n  creator: 'Slson',\n  twitter: {\n    creator: '@xSls0n_007',\n    title:\n      'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',\n    description:\n      'Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics for websites, social media, and more. With Prismify, you get browser frames, gradient backgrounds, text, annotations.',\n    card: 'summary_large_image',\n    images: [\n      {\n        url: 'https://prismify.vercel.app/opengraph-image.jpg',\n        width: 1200,\n        height: 630,\n        alt: 'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',\n      },\n    ],\n  },\n  openGraph: {\n    title:\n      'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',\n    description:\n      'Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics for websites, social media, and more. With Prismify, you get browser frames, gradient backgrounds, text, annotations.',\n    siteName: 'Prismify',\n    url: 'https://prismify.vercel.app',\n    locale: 'en_US',\n    type: 'website',\n    images: [\n      {\n        url: 'https://prismify.vercel.app/opengraph-image.jpg',\n        width: 1200,\n        height: 630,\n        alt: 'Prismify - Effortlessly Create Beautiful SaaS/Product Shots & Graphics',\n      },\n    ],\n  },\n  category: 'Design',\n  alternates: {\n    canonical: 'https://prismify.vercel.app',\n  },\n  keywords: [\n    'SaaS product design',\n    'Create beautiful screenshots',\n    'Graphics design for social media',\n    'Add browser frames',\n    'Gradient backgrounds for graphics',\n    'Text annotations tool',\n    'Beautiful website graphics',\n    'Enhance social media visuals',\n    'Website demo image creator',\n    'Visual content enhancement',\n    'SaaS branding tool',\n    'Design beautiful image for products',\n    'Prismify graphics maker',\n    'SaaS graphics design',\n    'Website screenshot generator',\n    'SaaS marketing visuals',\n  ],\n  metadataBase: new URL('https://prismify.vercel.app'),\n  robots: 'index, follow',\n  manifest: '/manifest.json',\n}\n\nexport const viewport: Viewport = {\n  themeColor: '#151515',\n  colorScheme: 'dark',\n}\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return (\n    <html\n      suppressHydrationWarning\n      className={cn('dark antialiased', Font.className)}\n      lang=\"en\"\n    >\n      <body className=\"min-h-screen bg-[#111] text-dark\">\n        <Toaster />\n        <Analytics />\n\n        <Providers>\n          <Navbar />\n          {children}\n        </Providers>\n\n        <ClarityScript />\n      </body>\n    </html>\n  )\n}\n"
  },
  {
    "path": "app/loading.tsx",
    "content": "import Loader from '@/components/loader'\n\nexport default function Loading() {\n  return <Loader />\n}\n"
  },
  {
    "path": "app/manifest.json",
    "content": "{\n  \"name\": \"Prismify\",\n  \"short_name\": \"Prismify\",\n  \"start_url\": \"/\",\n  \"display\": \"standalone\",\n  \"background_color\": \"#111111\",\n  \"theme_color\": \"#151515\",\n  \"description\": \"Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics.\",\n  \"icons\": [\n    {\n      \"src\": \"/favicons/apple-icon.png\",\n      \"sizes\": \"180x180\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"/favicons/favicon.ico\",\n      \"sizes\": \"48x48 64x64 96x96 128x128 256x256\",\n      \"type\": \"image/x-icon\"\n    }\n  ]\n}\n"
  },
  {
    "path": "app/not-found.tsx",
    "content": "/* eslint-disable react/no-unescaped-entities */\n'use client'\n\nimport { buttonVariants } from '@/components/ui/button'\nimport { cn } from '@/utils/button-utils'\nimport { MoveLeft } from 'lucide-react'\nimport Link from 'next/link'\n\nexport default function NotFound() {\n  return (\n    <div className=\"flex flex-col items-center px-4 pt-40 text-center sm:px-6 lg:px-8\">\n      <h1 className=\"block bg-gradient-to-br from-[#898AEB] via-[#898dd9]/80 to-[#8e8ece] bg-clip-text text-center text-4xl font-bold text-transparent sm:text-6xl \">\n        Lost in the Pixels.\n      </h1>\n      <br />\n      <p className=\"max-w-xl text-center text-lg font-medium text-foreground opacity-90 md:text-xl\">\n        <strong className=\"font-semibold\">Not found!</strong> Looks like you've\n        wandered off the grid.\n      </p>\n\n      <div className=\"mt-10 flex flex-col items-center justify-center gap-2 sm:flex-row sm:gap-3\">\n        <Link\n          href=\"/\"\n          className={cn(\n            buttonVariants({ variant: 'default' }),\n            'w-48 px-4 text-base sm:w-fit'\n          )}\n        >\n          <MoveLeft className=\"mr-2 h-5 w-5 text-foreground/80\" />\n          Back to home\n        </Link>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/page.tsx",
    "content": "import Sidebar from '@/components/editor/sidebar'\nimport Canvas from '@/components/editor/canvas-area'\n\nexport default function Home() {\n  return (\n    <main className=\"flex h-screen w-screen pt-[72px] sm:flex-row\">\n      <Sidebar />\n      <Canvas />\n    </main>\n  )\n}\n"
  },
  {
    "path": "app/robots.ts",
    "content": "import { MetadataRoute } from 'next'\n\nexport default function robots(): MetadataRoute.Robots {\n  return {\n    rules: {\n      userAgent: '*',\n      allow: '/',\n      disallow: '/admin/',\n    },\n    sitemap: [\n      'https://prismify.vercel.app/sitemap.xml',\n      'https://prismify.vercel.app/articles/sitemap.xml',\n    ],\n  }\n}\n"
  },
  {
    "path": "app/sitemap.ts",
    "content": "import { MetadataRoute } from 'next'\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n  const sitemapUrls: MetadataRoute.Sitemap = allStaticRoutes.map((p) => {\n    return {\n      url: `https://prismify.vercel.app/${p}`,\n      lastModified: '2024-01-13T12:17:43.023Z',\n      changeFrequency: p === '' ? 'monthly' : 'weekly',\n    }\n  })\n\n  return sitemapUrls\n}\n\nconst allStaticRoutes = ['', 'about', 'articles']\n"
  },
  {
    "path": "components/articles/article-card.tsx",
    "content": "import Image from 'next/image'\nimport Link from 'next/link'\n\ntype ArticleCardProps = {\n  title: string\n  category: string\n  image: string\n  date: string\n  href: string\n}\n\nexport default function ArticleCard({\n  title,\n  image,\n  date,\n  category,\n  href,\n}: ArticleCardProps) {\n  return (\n    <div className=\"group  flex flex-col items-center text-dark\">\n      <Link\n        href={href}\n        className=\"h-[300px] w-full overflow-hidden rounded-xl bg-formDark\"\n      >\n        <Image\n          src={image}\n          alt={title}\n          width={500}\n          height={300}\n          className=\"aspect-[4/3] h-full w-full object-cover object-center transition-all will-change-auto duration-300 group-hover:scale-105\"\n        />\n      </Link>\n\n      <div className=\"mt-4 flex w-full flex-col\">\n        <span className=\"text-xs font-semibold uppercase text-purple sm:text-sm\">\n          {category}\n        </span>\n        <Link href={href} className=\"my-1.5 inline-block\">\n          <h2 className=\"line-clamp-2 text-base font-semibold capitalize text-dark sm:text-lg\">\n            {title}\n          </h2>\n        </Link>\n\n        <span className=\"dark:text-light/50 max-w-xs text-sm font-medium capitalize text-dark/60  sm:text-base\">\n          {date}\n        </span>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/auth-modal.tsx",
    "content": "import * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'\nimport { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'\nimport { useMediaQuery } from '@/hooks/use-media-query'\nimport { LogIn } from 'lucide-react'\nimport SignInForm from './sign-in-form'\n\nexport function AuthModal() {\n  const [open, setOpen] = React.useState(false)\n  const isDesktop = useMediaQuery('(min-width: 768px)')\n\n  if (isDesktop) {\n    return (\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogTrigger asChild>\n          <Button\n            className=\"rounded-xl text-[13.6px]\"\n            size=\"sm\"\n            variant=\"default\"\n          >\n            <p className=\"hidden md:block\">Login</p>\n            <LogIn size={18} className=\"flex-center md:ml-2\" />\n          </Button>\n        </DialogTrigger>\n        <DialogContent className=\"scale-110 p-16\">\n          <SignInForm />\n        </DialogContent>\n      </Dialog>\n    )\n  }\n\n  return (\n    <Drawer open={open} onOpenChange={setOpen}>\n      <DrawerTrigger asChild>\n        <Button\n          className=\"rounded-xl text-[13.6px]\"\n          size=\"sm\"\n          variant=\"default\"\n        >\n          <p className=\"hidden md:block\">Login</p>\n          <LogIn size={18} className=\"flex-center md:ml-2\" />\n        </Button>\n      </DrawerTrigger>\n      <DrawerContent className=\"mb-6 rounded-xl bg-[#121212] px-6 py-2\">\n        <div className=\"mt-8\" />\n        <SignInForm />\n        <div className=\"mt-4\" />\n      </DrawerContent>\n    </Drawer>\n  )\n}\n"
  },
  {
    "path": "components/clarity-script.tsx",
    "content": "import Script from 'next/script'\n\nconst ClarityScript = () => (\n  <Script id=\"clarity-script\" strategy=\"afterInteractive\">\n    {`\n            (function(c,l,a,r,i,t,y){\n                c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};\n                t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;\n                y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);\n            })(window, document, \"clarity\", \"script\", \"${process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID}\");\n          `}\n  </Script>\n)\n\nexport default ClarityScript\n"
  },
  {
    "path": "components/color-picker.tsx",
    "content": "'use client'\n\nimport { useState } from 'react'\nimport {\n  HexAlphaColorPicker,\n  HexColorInput,\n  HexColorPicker,\n} from 'react-colorful'\n\nexport default function ColorPicker({\n  onChange,\n  colorState,\n  shouldShowAlpha = true,\n}: {\n  onChange: (color: string) => void\n  colorState: string\n  shouldShowAlpha?: boolean\n}) {\n  const [color, setColor] = useState(colorState)\n\n  return (\n    <>\n      <div className=\"flex-center flex-col gap-2\">\n        {shouldShowAlpha ? (\n          <HexAlphaColorPicker\n            color={color}\n            onChange={(color) => {\n              setColor(color)\n              onChange(color)\n            }}\n          />\n        ) : (\n          <HexColorPicker\n            color={color}\n            onChange={(color) => {\n              setColor(color)\n              onChange(color)\n            }}\n          />\n        )}\n        <div className=\"flex-center relative h-full w-full rounded-md border border-[#22262b] bg-formDark text-center text-sm uppercase text-gray-100 md:text-sm\">\n          <span className=\"absolute left-2 font-medium text-gray-400\">#</span>\n          <HexColorInput\n            color={color}\n            onChange={(color) => {\n              setColor(color)\n              onChange(color)\n            }}\n            className=\"h-full w-full rounded-md border-[#22262b] bg-formDark px-3 py-3 text-center text-gray-100 focus:border-[#8e8ece] focus:outline-none focus:ring-1 focus:ring-[#8e8ece] md:text-sm\"\n          />\n        </div>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/background-image-canvas.tsx",
    "content": "// this component shows background image & noise in the canvas\n\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport Noise from '@/components/editor/noise'\n\nexport default function BackgroundImageCanvas() {\n  const { imageBackground } = useBackgroundOptions()\n\n  return (\n    <>\n      {imageBackground && (\n        // eslint-disable-next-line @next/next/no-img-element\n        <img\n          draggable={false}\n          className={`pointer-events-none absolute z-[0] h-full w-full object-cover`}\n          src={imageBackground}\n          alt=\"background image\"\n        />\n      )}\n      <Noise />\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/background-options/custom-gradient-picker.tsx",
    "content": "'use client'\n\nimport { useCallback } from 'react'\nimport { solidColors } from '@/utils/presets/solid-colors'\nimport { Button } from '@/components/ui/button'\nimport PopupColorPicker from '@/components/popup-color-picker'\nimport { useBackgroundOptions } from '@/store/use-background-options'\n\nexport default function CustomGradientPicker() {\n  const {\n    setBackground,\n    background,\n    setBackgroundType,\n    solidColor,\n    setSolidColor,\n    setImageBackground,\n    imageBackground,\n  } = useBackgroundOptions()\n\n  const updateRootStyles = useCallback((color: string) => {\n    if (typeof window === 'undefined') return\n    document?.documentElement.style.setProperty('--solid-bg', color)\n    document?.documentElement.style.setProperty('--gradient-bg', color)\n    document?.documentElement.style.setProperty('--mesh-bg', color)\n  }, [])\n\n  const handleColorChange = useCallback(\n    (color: string) => {\n      setBackgroundType('solid')\n      setSolidColor(color)\n      setBackground(color)\n      setImageBackground(null)\n      updateRootStyles(color)\n    },\n    [\n      setSolidColor,\n      setBackground,\n      setBackgroundType,\n      updateRootStyles,\n      setImageBackground,\n    ]\n  )\n\n  return (\n    <>\n      <div>\n        <h3 className=\"mb-3 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n          <span>Pick a color:</span>\n        </h3>\n        <PopupColorPicker onChange={handleColorChange} color={solidColor} />\n      </div>\n\n      <div>\n        <h3 className=\"mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n          <span>Solid Colors:</span>\n        </h3>\n\n        <div className=\"mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid w-full\">\n          {solidColors.slice(0, 1).map(({ background: solidBackground }) => {\n            return (\n              <Button\n                key={solidBackground}\n                className={`aspect-square overflow-hidden rounded-sm p-0 ${\n                  background === solidBackground &&\n                  !imageBackground &&\n                  'outline-none ring-2 ring-ring ring-offset-2'\n                }`}\n                onClick={() => handleColorChange(solidBackground)}\n                style={{ background: solidBackground }}\n              >\n                {/* eslint-disable-next-line @next/next/no-img-element */}\n                <img\n                  className=\"h-full w-full scale-150\"\n                  src=\"/images/transparent.jpg\"\n                  alt=\"transparent background\"\n                />\n              </Button>\n            )\n          })}\n          {solidColors.slice(1).map(({ background: solidBackground }) => {\n            return (\n              <Button\n                key={solidBackground}\n                variant=\"secondary\"\n                className={`h-[2.56rem] w-[2.56rem] rounded-md ${\n                  background === solidBackground &&\n                  !imageBackground &&\n                  'outline-none ring-2 ring-ring ring-offset-2'\n                }`}\n                onClick={() => handleColorChange(solidBackground)}\n                style={{ background: solidBackground }}\n              />\n            )\n          })}\n        </div>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/background-options/image-gradient-picker.tsx",
    "content": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport { Switch } from '@/components/ui/switch'\nimport { toast } from '@/hooks/use-toast'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport { useQuery } from '@tanstack/react-query'\nimport { ChevronLeft, ChevronRight, Settings2 } from 'lucide-react'\nimport { Key, useEffect, useState } from 'react'\n\nexport default function ImageGradientPicker() {\n  const {\n    setImageBackground,\n    imageBackground,\n    setAttribution,\n    setHighResBackground,\n    highResBackground,\n    setBackgroundType,\n  } = useBackgroundOptions()\n  const [currentPage, setCurrentPage] = useState(1)\n\n  const fetchUnsplashPictures = async (page: number) => {\n    const response = await fetch(\n      `https://api.unsplash.com/collections/5wgHcmn38m4/photos?page=${page}&per_page=30&q=100&fit=clip&w=1500&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`\n    )\n    const data = await response.json()\n    return data\n  }\n\n  const {\n    isLoading,\n    isError,\n    refetch,\n    data: unsplashData,\n    error,\n  } = useQuery({\n    queryKey: ['unsplash-gradients'],\n    queryFn: () => fetchUnsplashPictures(currentPage),\n  })\n\n  useEffect(\n    () => {\n      refetch()\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [currentPage]\n  )\n\n  if (isLoading) {\n    const skeletonLoaders = Array.from({ length: 30 }).map((_, index) => (\n      <li\n        className={`h-[2.56rem] w-[2.56rem] rounded-md`}\n        key={`skeleton-${index}`}\n      >\n        <Skeleton className=\"h-full w-full rounded-md\" />\n      </li>\n    ))\n\n    return (\n      <>\n        <h3 className=\"mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n          <span>Images:</span>\n        </h3>\n        <ul className=\"mt-4 grid max-w-[18rem] auto-rows-auto grid-cols-5 gap-4\">\n          {skeletonLoaders}\n        </ul>\n      </>\n    )\n  }\n\n  if (isError && error instanceof Error) {\n    toast({\n      title: 'Error',\n      description: error.message,\n      variant: 'destructive',\n    })\n    return <span>Error: {error.message}</span>\n  }\n\n  return (\n    <>\n      <h3 className=\"mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n        <span>Images:</span>\n        <Popover>\n          <PopoverTrigger asChild>\n            <Settings2 size={20} />\n          </PopoverTrigger>\n          <PopoverContent className=\"flex w-fit flex-wrap gap-3\">\n            <h1 className=\"text-[0.85rem]\">High resolution background</h1>\n            <Switch\n              checked={highResBackground}\n              onCheckedChange={(checked) => {\n                setHighResBackground(checked)\n              }}\n            />\n          </PopoverContent>\n        </Popover>\n      </h3>\n\n      <ul className=\"mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid\">\n        {unsplashData.map(\n          (data: {\n            user: any\n            links: any\n            id: Key | null | undefined\n            urls: {\n              regular: string | null\n              small_s3: string | undefined\n              full: string | undefined\n            }\n            alt_description: string | undefined\n          }) => (\n            <li className={`h-[2.56rem] w-[2.56rem] rounded-md`} key={data.id}>\n              <button\n                className={`h-full w-full rounded-md ${\n                  imageBackground ===\n                    (highResBackground\n                      ? `${data.urls.full}`\n                      : `${data.urls.regular}`) &&\n                  'outline-none ring-2 ring-ring ring-offset-2'\n                }`}\n                onClick={() => {\n                  setBackgroundType('gradient')\n                  setImageBackground(\n                    highResBackground\n                      ? `${data.urls.full}`\n                      : `${data.urls.regular}`\n                  )\n                  setAttribution({\n                    name: data.user.first_name,\n                    link: data.user.username,\n                  })\n                  // Just triggering a download (Unsplash guideline)\n                  fetch(\n                    `${data.links.download_location}&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`\n                  )\n                }}\n              >\n                {/* eslint-disable-next-line @next/next/no-img-element */}\n                <img\n                  className=\"h-full w-full rounded-md object-cover\"\n                  src={data.urls.small_s3!}\n                  alt={data.alt_description}\n                />\n              </button>\n            </li>\n          )\n        )}\n      </ul>\n\n      <div className=\"flex justify-end mt-6 gap-2\">\n        <Button\n          size=\"sm\"\n          variant={'stylish'}\n          disabled={currentPage === 1}\n          className=\"text-sm flex-center h-9 px-2.5\"\n          onClick={() => {\n            setCurrentPage((prevPage) => prevPage - 1)\n          }}\n        >\n          <ChevronLeft size={16} className=\"mr-1 translate-y-[1px]\" />\n          <p>Back</p>\n        </Button>\n        <Button\n          variant={'stylish'}\n          disabled={currentPage === 3}\n          className=\"text-sm flex-center h-9 px-2.5\"\n          onClick={() => {\n            setCurrentPage((prevPage) => prevPage + 1)\n          }}\n        >\n          <p>Next</p> \n          <ChevronRight size={16} className=\"ml-1 translate-y-[1px]\" />\n        </Button>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/background-options/index.tsx",
    "content": "import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport CustomGradientPicker from './custom-gradient-picker'\nimport NormalGradientPicker from './normal-gradient-picker'\nimport NoiseSlider from './noise-slider'\nimport PatternPicker from './pattern-picker'\nimport { useEffect } from 'react'\n\nexport default function BackgroundOptions() {\n  const { backgroundType, setIsBackgroundClicked } = useBackgroundOptions()\n\n  useEffect(() => {\n    setIsBackgroundClicked(true)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <Tabs\n      className=\"mt-4 !border-none !shadow-none !bg-transparent w-full\"\n      defaultValue={\n        backgroundType === 'solid'\n          ? 'customTab'\n          : backgroundType === 'pattern'\n          ? 'patternsTab'\n          : 'gradientsTab'\n      }\n    >\n      <TabsList className=\"mb-4 bg-transparent border-none border-b border-border [&>*]:px-[1rem] [&>*]:border-b-2 [&>*]:border-border/40 [&>*]:pb-2 [&>*]:rounded-none [&>*[data-state=active]]:border-foreground [&>*[data-state=active]]:bg-transparent *:w-fit\">\n        <TabsTrigger value=\"gradientsTab\">Gradient</TabsTrigger>\n        <TabsTrigger value=\"patternsTab\">Abstract</TabsTrigger>\n        <TabsTrigger className='hidden sm:flex' value=\"customTab\">\n          Pick\n        </TabsTrigger>\n      </TabsList>\n      <NoiseSlider />\n      <TabsContent value=\"gradientsTab\">\n        <NormalGradientPicker />\n      </TabsContent>\n      <TabsContent value=\"patternsTab\">\n        <PatternPicker />\n      </TabsContent>\n      <TabsContent value=\"customTab\">\n        <CustomGradientPicker />\n      </TabsContent>\n    </Tabs>\n  )\n}\n"
  },
  {
    "path": "components/editor/background-options/noise-slider.tsx",
    "content": "import { Slider } from '@/components/ui/slider'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport { Grip } from 'lucide-react'\n\nexport default function NoiseSlider() {\n  const { noise, setNoise } = useBackgroundOptions()\n\n  return (\n    <div>\n      <div className=\"mb-3 mt-4 flex items-center px-1\">\n        <h3 className=\"mb-3 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n          <Grip size={18} />\n          <span>Noise</span>\n        </h3>\n      </div>\n\n      <div className=\"mb-3 flex gap-4 text-[0.85rem]\">\n        <Slider\n          defaultValue={[0]}\n          max={0.8}\n          min={0}\n          step={0.05}\n          value={[noise]}\n          onValueChange={(value: number[]) => {\n            setNoise(value[0])\n          }}\n          onIncrement={() => {\n            if (noise >= 0.8) return\n            setNoise(noise + 0.05)\n          }}\n          onDecrement={() => {\n            if (noise <= 0) return\n            setNoise(noise - 0.05)\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/background-options/normal-gradient-picker.tsx",
    "content": "'use client'\n\nimport { useState } from 'react'\n\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport CircularSliderComp from '@/components/ui/circular-slider'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport SpotlightButton from '@/components/ui/spotlight-button'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { gradients, type Gradient } from '@/utils/presets/gradients'\nimport ColorThief from 'colorthief'\nimport { Settings2, Sparkles, Zap, Palette, Wand2, Check, Rocket } from 'lucide-react'\nimport { useCallback } from 'react'\nimport ImageGradientPicker from './image-gradient-picker'\nimport { cn } from '@/utils/button-utils'\n\ntype Color = string\n\nexport default function NormalGradientPicker() {\n  const {\n    setBackground,\n    background: backgroundInStore,\n    setBackgroundType,\n    backgroundType,\n    setImageBackground,\n    imageBackground,\n    setAttribution,\n    setNoise,\n  } = useBackgroundOptions()\n\n  const { images, setImages } = useImageOptions()\n  const { selectedImage } = useSelectedLayers()\n  const [dominantColor, setDominantColor] = useState(null)\n  const [isGenerating, setIsGenerating] = useState(false)\n\n  const handleGradientClick = useCallback(\n    (gradient: Gradient, isMesh: boolean) => {\n      if (typeof window === 'undefined') return\n      document?.documentElement.style.setProperty(\n        '--gradient-bg',\n        gradient.gradient\n      )\n      document?.documentElement.style.setProperty(\n        '--mesh-bg',\n        isMesh ? gradient.background! : gradient.gradient\n      )\n      setBackground(gradient.gradient)\n      setBackgroundType(isMesh ? 'mesh' : 'gradient')\n      setImageBackground(null)\n      setAttribution({ name: null, link: null })\n    },\n    [setBackground, setBackgroundType, setImageBackground, setAttribution]\n  )\n\n  const extractDominantColor = async () => {\n    setIsGenerating(true)\n    const colorThief = new ColorThief()\n    const image = new Image()\n    image.crossOrigin = 'Anonymous'\n    image.src =\n      document.getElementById(`img-${selectedImage}`)?.getAttribute('src') ?? ''\n\n    image.onload = function () {\n      const palletes = colorThief.getPalette(image, 8)\n      const dominantColor = colorThief.getColor(image)\n\n      function generateGradients(\n        dominantColor: Color,\n        colors: Color[]\n      ): { linearGradients: string[]; radialGradients: string[]; meshGradients: string[] } {\n        const getRandomColors = (limit: number): Color[] => {\n          const randomColors: Color[] = []\n          for (let i = 0; i < limit; i++) {\n            const randomIndex = Math.floor(Math.random() * colors.length)\n            randomColors.push(colors[randomIndex])\n          }\n          return randomColors\n        }\n\n        // Linear Gradients - Clean, directional gradients with 2-3 colors\n        const generateLinearGradients = (): string[] => {\n          const gradients: string[] = []\n          \n          // Dominant to each palette color\n          colors.forEach((color: Color, index: number) => {\n            gradients.push(`linear-gradient(${135 + index * 30}deg, ${dominantColor} 0%, ${color} 100%)`)\n          })\n          \n          // Two-color combinations from palette\n          for (let i = 0; i < 4; i++) {\n            const [color1, color2] = getRandomColors(2)\n            gradients.push(`linear-gradient(${45 + i * 45}deg, ${color1} 0%, ${color2} 100%)`)\n          }\n          \n          // Three-color smooth transitions\n          for (let i = 0; i < 3; i++) {\n            const [color1, color2, color3] = getRandomColors(3)\n            gradients.push(`linear-gradient(${90 + i * 60}deg, ${color1} 0%, ${color2} 50%, ${color3} 100%)`)\n          }\n          \n          return gradients.slice(0, 14)\n        }\n\n        // Radial Gradients - Circular, spotlight effects\n        const generateRadialGradients = (): string[] => {\n          const gradients: string[] = []\n          \n          // Center spotlight with dominant color\n          colors.forEach((color: Color) => {\n            gradients.push(`radial-gradient(circle at 50% 50%, ${dominantColor} 0%, ${color} 100%)`)\n          })\n          \n          // Off-center radials for dynamic feel\n          const positions = ['20% 30%', '80% 20%', '30% 80%', '70% 70%']\n          positions.forEach((pos, i) => {\n            const [color1, color2] = getRandomColors(2)\n            gradients.push(`radial-gradient(circle at ${pos}, ${color1} 0%, ${color2} 60%, transparent 100%)`)\n          })\n          \n          // Elliptical radials\n          for (let i = 0; i < 3; i++) {\n            const [color1, color2] = getRandomColors(2)\n            gradients.push(`radial-gradient(ellipse at ${50 + i * 20}% 50%, ${color1} 0%, ${color2} 100%)`)\n          }\n          \n          return gradients.slice(0, 14)\n        }\n\n        // Mesh Gradients - Complex, multi-layered beautiful gradients\n        const generateMeshGradients = (): string[] => {\n          const gradients: string[] = []\n          \n          // Beautiful mesh gradients with multiple radial layers\n          for (let i = 0; i < 14; i++) {\n            const meshColors = getRandomColors(4)\n            const positions = [\n              { x: 10 + Math.random() * 30, y: 10 + Math.random() * 30 },\n              { x: 60 + Math.random() * 30, y: 10 + Math.random() * 30 },\n              { x: 10 + Math.random() * 30, y: 60 + Math.random() * 30 },\n              { x: 60 + Math.random() * 30, y: 60 + Math.random() * 30 }\n            ]\n            \n            const meshGradient = `\n              radial-gradient(circle at ${positions[0].x}% ${positions[0].y}%, ${meshColors[0]} 0%, transparent 50%),\n              radial-gradient(circle at ${positions[1].x}% ${positions[1].y}%, ${meshColors[1]} 0%, transparent 50%),\n              radial-gradient(circle at ${positions[2].x}% ${positions[2].y}%, ${meshColors[2]} 0%, transparent 50%),\n              radial-gradient(circle at ${positions[3].x}% ${positions[3].y}%, ${meshColors[3]} 0%, transparent 50%),\n              linear-gradient(${45 + i * 15}deg, ${dominantColor} 0%, ${meshColors[0]} 100%)\n            `.trim()\n            \n            gradients.push(meshGradient)\n          }\n          \n          return gradients\n        }\n\n        const linearGradients = generateLinearGradients()\n        const radialGradients = generateRadialGradients()\n        const meshGradients = generateMeshGradients()\n\n        setImages(\n          images.map((image, index) =>\n            index === images.length - 1\n              ? {\n                  ...image,\n                  dominantColor,\n                  palletes,\n                  linearGradients,\n                  meshGradients,\n                  radialGradients,\n                }\n              : image\n          )\n        )\n\n        setTimeout(() => setIsGenerating(false), 800)\n\n        return {\n          linearGradients,\n          radialGradients,\n          meshGradients,\n        }\n      }\n\n      generateGradients(\n        // @ts-ignore\n        `rgb(${dominantColor.join(',')})`,\n        // @ts-ignore\n        palletes.map((pallete) => `rgb(${pallete.join(',')})`)\n      )\n    }\n  }\n\n  const GradientSection = ({ title, gradients, icon: Icon, description }: { \n    title: string; \n    gradients: string[]; \n    icon: any; \n    description: string;\n  }) => (\n    <div className=\"group relative\">\n      <div className=\"mb-4 flex items-center justify-between\">\n        <div className=\"flex items-center gap-2\">\n          <Icon className=\"h-4 w-4 text-purple/50\" />\n          <h4 className=\"text-sm font-semibold text-foreground\">{title}</h4>\n          <Badge className=\"text-xs px-2 py-0.5 bg-primary/10 0 text-purple border border-primary/10\">\n            {gradients?.length}\n          </Badge>\n        </div>\n        <span className=\"text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity\">\n          {description}\n        </span>\n      </div>\n      <div className=\"grid grid-cols-7 gap-2\">\n        {gradients.map((gradient: string, index: number) => (\n          <Button\n            key={gradient}\n            type=\"button\"\n            variant=\"secondary\"\n            className={cn(\n              \"aspect-square h-[1.85rem] w-[1.85rem] overflow-hidden rounded-md p-[1px] transition-all duration-300\",\n              \"hover:scale-105 hover:shadow-lg hover:shadow-primary/25 hover:brightness-105\",\n              \" hover:ring-1 hover:ring-primary/20\",\n              \"transform-gpu active:scale-95 active:shadow-inner\",\n              \"group relative cursor-pointer\",\n              gradient === backgroundInStore && !imageBackground && \n              \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-105 shadow-lg shadow-primary/30 brightness-105\"\n            )}\n            onClick={() =>\n              handleGradientClick(\n                {\n                  gradient,\n                  background: gradient,\n                  type: title === 'Mesh' ? 'Mesh' : 'Normal',\n                },\n                title === 'Mesh'\n              )\n            }\n            style={{ background: gradient }}\n          >\n            {gradient === backgroundInStore &&\n              !imageBackground &&\n              backgroundType !== 'mesh' && (\n                <Rocket\n                      className=\"h-4 w-4 text-white drop-shadow-lg transition-all duration-1000 repeat-infinite animate-pulse\"\n                    />\n              )}\n          </Button>\n        ))}\n      </div>\n    </div>\n  )\n\n  console.log(dominantColor)\n\n  return (\n    <div>\n      {/* Premium Header */}\n      <div className=\"relative mt-8 overflow-hidden rounded-2xl border border-border/50 bg-gradient-to-br from-background via-muted/20 to-muted/40 p-4 backdrop-blur-sm\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-primary/5 via-primary/10 to-primary/5\" />\n        <div className=\"relative\">\n          <h4 className=\"font-medium text-foreground mb-1\">Adaptive Gradient</h4>\n          <p className=\"text-sm text-muted-foreground mb-4 max-w-md\">\n            Generate stunning gradients matched to your image&apos;s color palette. {selectedImage ? (\n              <>\n                Click on the{' '}\n                <button \n                  onClick={extractDominantColor}\n                  disabled={!selectedImage || isGenerating}\n                  className=\"font-medium text-purple hover:text-purple/80 underline decoration-purple/50 hover:decoration-purple transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n                >\n                  Generate\n                </button>\n                {' '}button to generate.\n              </>\n            ) : 'Please click on the image layer to generate.'}\n          </p>\n          \n          <div\n            className=\"transition-all duration-300\"\n          >\n            <TooltipProvider>\n              <Tooltip delayDuration={100}>\n                <TooltipTrigger asChild>\n                  <div className=\"inline-block\">\n                    <SpotlightButton\n                      as={!selectedImage ? 'div' : 'button'}\n                      onClick={extractDominantColor}\n                      text={isGenerating ? \"Analyzing...\" : (selectedImage && images[selectedImage - 1]?.linearGradients ? \"Regenerate\" : \"Generate\")}\n                      disabled={!selectedImage || isGenerating}\n                    />\n                  </div>\n                </TooltipTrigger>\n                <TooltipContent\n                  side=\"top\"\n                  align=\"start\"\n                  className=\"max-w-[10rem] z-[1000] text-center\"\n                  style={{\n                    display: selectedImage ? 'none' : 'block',\n                  }}\n                >\n                  <div className=\"flex items-center text-left gap-2 mb-2\">\n                    <span className=\"font-medium\">Image Required</span>\n\n                  </div>\n                  <p className=\"text-sm text-left\">\n                    Please click on the image layer on canvas first!\n                  </p>\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n\n                          {isGenerating && (\n                <div className=\"mt-4 flex items-center gap-2 text-sm text-muted-foreground\">\n                  <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent\" />\n                  <span>Analyzing color palette...</span>\n                </div>\n              )}\n          </div>\n        </div>\n      </div>\n\n      {/* Premium Gradient Collections */}\n      {selectedImage && images[selectedImage - 1]?.linearGradients && (\n        <div className=\"mt-8 space-y-8\">\n          <GradientSection\n            title=\"Linear\"\n            gradients={images[selectedImage - 1]?.linearGradients || []}\n            icon={Wand2}\n            description=\"Directional flow\"\n          />\n          \n          <GradientSection\n            title=\"Radial\"\n            gradients={images[selectedImage - 1]?.radialGradients || []}\n            icon={Sparkles}\n            description=\"Spotlight & circular\"\n          />\n          \n          <GradientSection\n            title=\"Mesh\"\n            gradients={images[selectedImage - 1]?.meshGradients || []}\n            icon={Palette}\n            description=\"Complex multi-layered\"\n          />\n\n          {/* Premium Tip */}\n          <div className=\"relative overflow-hidden rounded-xl border border-border/50 bg-gradient-to-r from-muted/30 to-muted/50 p-4\">\n            <div className=\"absolute inset-0 bg-gradient-to-r from-primary/5 to-primary/10\" />\n            <div className=\"relative flex items-start gap-3\">\n             \n              <div>\n                <h4 className=\"font-medium text-foreground mb-1\">Pro Tip</h4>\n                <p className=\"text-sm text-muted-foreground\">\n                  Mesh gradients work beautifully with{' '}\n                  <button \n                    onClick={() => setNoise(0.15)}\n                    className=\"font-medium text-purple hover:text-purple/80 underline decoration-purple/50 hover:decoration-purple transition-colors\"\n                  >\n                    15% noise\n                  </button>\n                  {' '}for that premium mesh effect.\n                </p>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n\n      <h3 className=\"mt-12 flex items-center gap-2 text-xs font-medium uppercase text-muted-foreground\">\n        <span>Standard Gradients</span>\n      </h3>\n\n      <div className=\"mt-4 flex w-full grid-cols-7 flex-wrap gap-[0.5rem] md:grid\">\n        {gradients.map(({ gradient, background, type }: Gradient) => (\n          <Button\n            key={gradient}\n            type=\"button\"\n            variant=\"secondary\"\n            className={`h-[1.85rem] w-[1.85rem] overflow-hidden rounded-md p-[1px] ${\n              gradient === backgroundInStore &&\n              !imageBackground &&\n              'outline-none ring-2 ring-ring ring-offset-2'\n            }`}\n            onClick={() =>\n              handleGradientClick(\n                {\n                  gradient,\n                  background,\n                  type: 'Normal',\n                },\n                type === 'Mesh' // will be true if its of type Mesh\n              )\n            }\n            style={\n              type === 'Normal'\n                ? { background: gradient }\n                : { backgroundColor: background, backgroundImage: gradient }\n            }\n          >\n            {gradient === backgroundInStore &&\n              !imageBackground &&\n              backgroundType !== 'mesh' && (\n                <Popover>\n                  <PopoverTrigger asChild>\n                    <Settings2 className=\"flex-center\" color=\"#333\" size={20} />\n                  </PopoverTrigger>\n                  <PopoverContent className=\"flex w-[12rem] flex-col items-center gap-3\">\n                    <h1 className=\"text-[0.85rem]\">Gradient angle</h1>\n                    <div className={`circular-slider`}>\n                      <CircularSliderComp />\n                    </div>\n                  </PopoverContent>\n                </Popover>\n              )}\n          </Button>\n        ))}\n      </div>\n\n      <ImageGradientPicker />\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/background-options/pattern-picker.tsx",
    "content": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport { Switch } from '@/components/ui/switch'\nimport { toast } from '@/hooks/use-toast'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport { useQuery } from '@tanstack/react-query'\nimport { Settings2 } from 'lucide-react'\nimport { Key, useState } from 'react'\n\nexport default function PatternPicker() {\n  const {\n    setImageBackground,\n    imageBackground,\n    setAttribution,\n    setHighResBackground,\n    highResBackground,\n    setBackgroundType,\n  } = useBackgroundOptions()\n  const [currentPage, setCurrentPage] = useState(1)\n\n  const fetchUnsplashPatterns = async (page: number) => {\n    const response = await fetch(\n      `https://api.unsplash.com/collections/W121KJsaTEs/photos?page=${page}&per_page=30&q=100&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`\n    )\n    const data = await response.json()\n    return data\n  }\n\n  const {\n    isLoading,\n    isError,\n    data: unsplashData,\n    error,\n  } = useQuery({\n    queryKey: ['unsplash-patterns', currentPage],\n    queryFn: () => fetchUnsplashPatterns(currentPage),\n  })\n\n  if (isLoading) {\n    const skeletonLoaders = Array.from({ length: 30 }).map((_, index) => (\n      <li\n        className={`h-[2.56rem] w-[2.56rem] rounded-md`}\n        key={`skeleton-${index}`}\n      >\n        <Skeleton className=\"h-full w-full rounded-md\" />\n      </li>\n    ))\n\n    return (\n      <>\n        <h3 className=\"mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n          <span>Abstract:</span>\n        </h3>\n        <ul className=\"mt-4 grid auto-rows-auto grid-cols-4 gap-4 md:max-w-[18rem] md:grid-cols-5\">\n          {skeletonLoaders}\n        </ul>\n      </>\n    )\n  }\n\n  if (isError && error instanceof Error) {\n    toast({\n      title: 'Error',\n      description: error.message,\n      variant: 'destructive',\n    })\n    return <span>Error: {error.message}</span>\n  }\n\n  return (\n    <>\n      <h3 className=\"mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n        <span>Abstract:</span>\n        <Popover>\n          <PopoverTrigger asChild>\n            <Settings2 size={20} className=\"rotate-90\" />\n          </PopoverTrigger>\n          <PopoverContent className=\"flex w-fit flex-col flex-wrap gap-3\">\n            <div className=\"flex gap-3\">\n              <h1 className=\"text-[0.85rem]\">High resolution background</h1>\n              <Switch\n                checked={highResBackground}\n                onCheckedChange={(checked) => {\n                  setHighResBackground(checked)\n                }}\n              />\n            </div>\n          </PopoverContent>\n        </Popover>\n      </h3>\n\n      <ul className=\"mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid\">\n        {unsplashData.map(\n          (data: {\n            user: any\n            links: any\n            id: Key | null | undefined\n            urls: {\n              regular: string | null\n              small_s3: string | undefined\n              full: string | undefined\n            }\n            alt_description: string | undefined\n          }) => (\n            <li className={`h-[2.56rem] w-[2.56rem] rounded-md`} key={data.id}>\n              <button\n                className={`h-full w-full rounded-md ${\n                  imageBackground ===\n                    (highResBackground\n                      ? `${data.urls.full}`\n                      : `${data.urls.regular}`) &&\n                  'outline-none ring-2 ring-ring ring-offset-2'\n                }`}\n                onClick={() => {\n                  setBackgroundType('pattern')\n                  setImageBackground(\n                    highResBackground\n                      ? `${data.urls.full}`\n                      : `${data.urls.regular}`\n                  )\n                  setAttribution({\n                    name: data.user.first_name,\n                    link: data.user.username,\n                  })\n                }}\n              >\n                {/* eslint-disable-next-line @next/next/no-img-element */}\n                <img\n                  className=\"h-full w-full rounded-md object-cover\"\n                  src={data.urls.small_s3!}\n                  alt={data.alt_description}\n                />\n              </button>\n            </li>\n          )\n        )}\n      </ul>\n\n      <div className=\"flex justify-end gap-2 md:max-w-[18rem]\">\n        <Button\n          size=\"sm\"\n          variant={'stylish'}\n          disabled={currentPage === 1}\n          className=\"mt-4 text-sm\"\n          onClick={() => {\n            setCurrentPage((prevPage) => prevPage - 1)\n          }}\n        >\n          &larr; Back\n        </Button>\n        <Button\n          size=\"sm\"\n          variant={'stylish'}\n          disabled={currentPage === 2}\n          className=\"mt-4 text-sm\"\n          onClick={() => {\n            setCurrentPage((prevPage) => prevPage + 1)\n          }}\n        >\n          Next &rarr;\n        </Button>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/browser-frames.tsx",
    "content": "// This component is responsible for rendering the browser frame around the image layer.\n\n'use client'\n\nimport { FrameTypes, useFrameOptions } from '@/store/use-frame-options'\nimport { cn } from '@/utils/button-utils'\n\nconst FrameButton = ({ color }: { color: string }) => (\n  <div\n    className={'rounded-full'}\n    style={{\n      backgroundColor: color,\n    }}\n  />\n)\n\nconst FrameButtons = ({\n  hasButtonColor,\n  frame,\n}: {\n  hasButtonColor: boolean\n  frame: FrameTypes\n}) => {\n  const { frameHeight } = useFrameOptions()\n  let colors: string[] = []\n\n  if (hasButtonColor && frame === 'MacOS Dark') {\n    colors = ['#f7645c', '#fbc341', '#3cc84a']\n  } else if (hasButtonColor && frame === 'MacOS Light') {\n    colors = ['#f7645ccc', '#fbc341d2', '#3cc84ac5']\n  } else if (!hasButtonColor && frame === 'MacOS Dark') {\n    colors = ['#ffffff33', '#ffffff33', '#ffffff33']\n  } else if (!hasButtonColor && frame === 'MacOS Light') {\n    colors = ['#00000033', '#00000033', '#00000033']\n  }\n\n  return (\n    <div\n      className={`mr-2 flex basis-[6%] ${\n        frameHeight === 'small'\n          ? 'gap-[0.6vw] [&>*]:h-[0.7vw] [&>*]:w-[0.7vw]'\n          : frameHeight === 'medium'\n          ? 'gap-[0.65vw] [&>*]:h-[0.8vw] [&>*]:w-[0.8vw]'\n          : 'gap-[0.7vw] [&>*]:h-[0.9vw] [&>*]:w-[0.9vw]'\n      }`}\n    >\n      {colors.map((color, index) => (\n        <FrameButton key={index} color={color} />\n      ))}\n    </div>\n  )\n}\n\nconst FrameSearchBar = ({ frame }: { frame: FrameTypes }) => (\n  <div\n    className={cn(\n      'flex h-[50%] w-full flex-1 items-center rounded-md px-2 opacity-5',\n      frame === 'MacOS Dark' ? 'bg-white' : 'bg-[#000]'\n    )}\n  />\n)\n\nconst FrameContainer = ({\n  frameHeight,\n  children,\n  style,\n  additionalClasses = '',\n}: {\n  frameHeight: string\n  children: React.ReactNode\n  style: React.CSSProperties\n  additionalClasses?: string\n}) => {\n  const heightClass =\n    frameHeight === 'small'\n      ? 'h-[2.6vw] px-[1.6vw]'\n      : frameHeight === 'medium'\n      ? 'h-[3vw] px-[1.8vw]'\n      : 'h-[3.4vw] px-[2vw]'\n\n  return (\n    <div\n      style={style}\n      className={`flex items-center gap-4 ${heightClass} ${additionalClasses}`}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport default function BrowserFrame({ frame }: { frame: FrameTypes }) {\n  const {\n    frameHeight,\n    showSearchBar,\n    macOsDarkColor,\n    macOsLightColor,\n    hideButtons,\n    hasButtonColor,\n  } = useFrameOptions()\n\n  const props = { frame }\n\n  const frameComponents = {\n    'MacOS Dark': (\n      <FrameContainer\n        style={{ background: macOsDarkColor }}\n        frameHeight={frameHeight}\n      >\n        {!hideButtons && (\n          <FrameButtons hasButtonColor={hasButtonColor} {...props} />\n        )}\n        {showSearchBar && <FrameSearchBar {...props} />}\n      </FrameContainer>\n    ),\n    'MacOS Light': (\n      <FrameContainer\n        style={{ background: macOsLightColor }}\n        frameHeight={frameHeight}\n        additionalClasses=\"border-b border-[#00000010]\"\n      >\n        {!hideButtons && (\n          <FrameButtons hasButtonColor={hasButtonColor} {...props} />\n        )}\n        {showSearchBar && <FrameSearchBar {...props} />}\n      </FrameContainer>\n    ),\n    None: null,\n    Arc: null,\n    Shadow: null,\n  }\n\n  return frameComponents[frame] || null\n}\n"
  },
  {
    "path": "components/editor/canvas-area.tsx",
    "content": "'use client'\n\nimport useAutomaticAspectRatioSwitcher from '@/hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher'\nimport useCanvasResizeObserver from '@/hooks/canvas-area-hooks/use-resize-observer'\nimport useScreenSizeWarningToast from '@/hooks/canvas-area-hooks/use-screen-size-warning-toast'\nimport { useEventListener } from '@/hooks/use-event-listener'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport dynamic from 'next/dynamic'\nimport React, { CSSProperties, useRef } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport BackgroundImageCanvas from './background-image-canvas'\nimport ImageUpload from './main-image-area'\nimport MobileViewImageOptions from './mobile-view-image-options'\nimport SelectoComponent from './selecto-component'\nimport TextLayers from './text-layers'\nimport TiptapMoveable from './tiptap-moveable'\n\nconst MoveableComponent = dynamic(\n  () => import('./moveable-component').then((mod) => mod.default),\n  { ssr: false }\n)\n\nexport default function Canvas() {\n  const { backgroundType } = useBackgroundOptions()\n  const {\n    resolution,\n    exactDomResolution,\n    scrollScale,\n    setScrollScale,\n    canvasRoundness,\n  } = useResizeCanvas()\n  const { scale } = useImageOptions()\n  const {\n    selectedImage,\n    selectedText,\n    setSelectedImage,\n    enableCrop,\n    setSelectedText,\n  } = useSelectedLayers()\n  const screenshotRef = useRef<HTMLDivElement | null>(null)\n  const parentRef = useRef<HTMLDivElement | null>(null)\n  const {\n    showControls,\n    setShowControls,\n    setShowTextControls,\n    setIsMultipleTargetSelected,\n    showTextControls,\n    isEditable,\n    setIsEditable,\n    isSelecting,\n  } = useMoveable()\n\n  const [width, height]: number[] = resolution.split('x').map(Number)\n\n  const aspectRatio = width / height\n\n  console.log(`Current DOM resoltion: ${exactDomResolution}`)\n\n  let style: CSSProperties = {\n    aspectRatio,\n    backgroundImage: `var(--gradient-bg)`,\n    borderRadius: `${canvasRoundness * 16}px`,\n  }\n\n  if (backgroundType === 'mesh') {\n    style = {\n      ...style,\n      backgroundColor: `var(--mesh-bg)`,\n    }\n  }\n\n  if (backgroundType === 'solid') {\n    style = {\n      ...style,\n      backgroundColor: `var(--solid-bg)`,\n    }\n  }\n\n  // This hook encapsulates the logic for observing changes in the size of the screenshot element & automatically sets the DOM resolution and scale factor based on the size changes of a provided ref element.\n  useCanvasResizeObserver(screenshotRef)\n\n  // This hook encapsulates the logic used to automatically switch the aspect ratio of a screenshot within a container. If the screenshot overflows the container, the aspect ratio is adjusted to fit within the container.\n  useAutomaticAspectRatioSwitcher({\n    containerRef: parentRef,\n    screenshotRef,\n  })\n\n  // This hook shows a warning toast if the screen size is less than 768px.\n  useScreenSizeWarningToast()\n\n  const handleScroll = (e: React.WheelEvent<HTMLDivElement>) => {\n    if (typeof window !== 'undefined' && window.innerWidth <= 768) {\n      return\n    }\n    if (enableCrop) return\n    if (e.deltaY < 0) {\n      // Scrolling up\n      if (scrollScale === 1) return\n      setScrollScale(scrollScale + 0.1) // Increment the scroll scale by 0.1\n    } else if (e.deltaY > 0) {\n      // Scrolling down\n      if (scrollScale <= 0.4) return\n      setScrollScale(scrollScale - 0.1) // Decrement the scroll scale by 0.1\n    }\n  }\n\n  let parentScaleStyle = {\n    scale: `${scrollScale}`,\n  }\n\n  // Close the image controls when the escape key is pressed\n  useHotkeys('Escape', () => {\n    if (showControls) {\n      setShowControls(false)\n      setIsMultipleTargetSelected(false)\n    }\n  })\n\n  // this hook listens for a click event on the canvas and hides the image controls/text controls if the user clicks outside the image. This just works idk how, I suggest you don't touch it.\n  useEventListener(\n    'click',\n    (e: any) => {\n      const isCanvasArea =\n        e?.target?.classList?.contains('canvas-container') ||\n        e?.target?.classList?.contains('selecto-area')\n\n      if (isCanvasArea) {\n        setSelectedText(null)\n        setShowTextControls(false)\n        setShowTextControls(false)\n        setIsEditable(false)\n      }\n\n      if (isSelecting || (!selectedImage && !showControls)) return\n      if (isCanvasArea) {\n        setSelectedImage(null)\n        setShowTextControls(false)\n        setShowControls(false)\n        setIsMultipleTargetSelected(false)\n      }\n    },\n    screenshotRef\n  )\n\n  return (\n    <>\n      <section\n        ref={parentRef}\n        className={`relative flex h-full w-full flex-col overflow-hidden bg-[#111] md:grid md:place-items-center ${\n          aspectRatio <= 1 ? 'p-4 md:p-8' : 'p-4 md:p-8'\n        }\n        `}\n        style={parentScaleStyle}\n        onWheel={handleScroll}\n      >\n        <div\n          className={`canvas-container relative flex items-center justify-center overflow-hidden ${\n            aspectRatio <= 1\n              ? 'h-auto w-full lg:h-full lg:w-auto'\n              : 'h-auto w-full'\n          }`}\n          ref={screenshotRef}\n          id=\"canvas-container\"\n          style={style}\n        >\n          <BackgroundImageCanvas />\n          {showControls && <MoveableComponent id={`${selectedImage}`} />}\n          {showTextControls && !isEditable && (\n            <TiptapMoveable id={`text-${selectedText}`} />\n          )}\n\n          <div\n            className=\"selecto-area relative flex h-full w-full place-items-center items-center justify-center\"\n            style={{\n              scale,\n            }}\n          >\n            <ImageUpload />\n            <TextLayers />\n          </div>\n        </div>\n        <MobileViewImageOptions />\n      </section>\n\n      <SelectoComponent />\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/canvas-options/canvas-roundness-slider.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport { RotateCcw } from 'lucide-react'\n\nexport default function CanvasRoundnessSlider() {\n  const { canvasRoundness, setCanvasRoundness } = useResizeCanvas()\n\n  return (\n    <>\n      <div className=\"mb-3 mt-4 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Roundness</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round((canvasRoundness / 3) * 100)} `}\n        </p>\n        <Button\n          aria-label=\"reset roundness\"\n          variant=\"secondary\"\n          size=\"sm\"\n          className=\"ml-auto translate-x-2\"\n          onClick={() => setCanvasRoundness(0)}\n        >\n          <RotateCcw size={15} className=\"text-dark/80\" />\n        </Button>\n      </div>\n      <div className=\"flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={3}\n          min={0}\n          step={0.01}\n          value={[canvasRoundness]}\n          onValueChange={(value: number[]) => {\n            setCanvasRoundness(value[0])\n          }}\n          onDecrement={() => {\n            if (canvasRoundness <= 0) return\n            setCanvasRoundness(canvasRoundness - 0.03)\n          }}\n          onIncrement={() => {\n            if (canvasRoundness >= 3) return\n            setCanvasRoundness(canvasRoundness + 0.03)\n          }}\n        />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/canvas-options/index.tsx",
    "content": "'use client'\n\nimport {\n  Dribbble,\n  Facebook,\n  Instagram,\n  Linkedin,\n  Plus,\n  Minus,\n  Twitter,\n  Youtube,\n  ArrowRight,\n} from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { resolutions } from '@/utils/presets/resolutions'\nimport { Button } from '@/components/ui/button'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport { Separator } from '@/components/ui/separator'\nimport { ResolutionButton } from './resolution-button'\nimport CanvasRoundnessSlider from './canvas-roundness-slider'\nimport { Input } from '@/components/ui/input'\nimport { Switch } from '@/components/ui/switch'\nimport {\n  TooltipProvider,\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from '@/components/ui/tooltip'\nimport Icon from '@/components/icons'\n\nconst icons = {\n  Youtube: <Youtube size={18} />,\n  Instagram: <Instagram size={18} />,\n  Facebook: <Facebook size={18} />,\n  LinkedIn: <Linkedin size={18} />,\n  Twitter: <Twitter size={18} />,\n  Dribble: <Dribbble size={18} />,\n  ProductHunt: (\n    <div className=\"flex-center h-6 w-6 rounded-full bg-[#898aeb]/5\">P</div>\n  ),\n}\n\nconst splitResolution = (resolution: string) => resolution.split('x')\n\nexport default function CanvasOptions() {\n  const {\n    setResolution,\n    domResolution,\n    scrollScale,\n    setScrollScale,\n    automaticResolution,\n    setAutomaticResolution,\n  } = useResizeCanvas()\n\n  const [width, height] = splitResolution(domResolution)\n\n  const [inputResolution, setInputResolution] = useState({\n    inputWidth: width,\n    inputHeight: height,\n  })\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault()\n    setResolution(\n      `${inputResolution.inputWidth}x${inputResolution.inputHeight}`\n    )\n  }\n\n  useEffect(() => {\n    setInputResolution({\n      inputWidth: `${Math.round(+width)}`,\n      inputHeight: `${Math.round(+height)}`,\n    })\n  }, [height, width])\n\n  return (\n    <>\n      <div className=\"mt-4 flex w-full justify-between px-1\">\n        <div className=\"flex-center\">\n          <h1 className=\"mr-1 text-[0.85rem]\">Auto resolution</h1>\n          <TooltipProvider>\n            <Tooltip delayDuration={100}>\n              <TooltipTrigger>\n                <Icon variant=\"duotone\" name=\"info\" color=\"none\" />\n              </TooltipTrigger>\n              <TooltipContent className=\"max-w-[12rem]\">\n                <p>\n                  When enabled, the canvas will automatically resize to fit your\n                  image when you upload it.\n                </p>\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        </div>\n        <Switch\n          checked={automaticResolution}\n          onCheckedChange={(checked) => {\n            setAutomaticResolution(checked)\n          }}\n        />\n      </div>\n\n      <hr className=\"my-4 border-border\" />\n\n      <h1 className=\"mb-3 mt-8 px-1 text-[0.85rem]\">Custom Resolution</h1>\n      <form\n        onSubmit={handleSubmit}\n        className=\"flex w-full max-w-sm items-center space-x-2\"\n      >\n        <Input\n          type=\"number\"\n          value={inputResolution.inputWidth}\n          min={100}\n          max={5000}\n          onChange={(e) => {\n            setInputResolution({\n              ...inputResolution,\n              inputWidth: e.target.value,\n            })\n          }}\n          className=\"rounded-lg text-sm\"\n        />\n        <span className=\"mx-2 my-auto\">x</span>\n        <Input\n          type=\"number\"\n          value={inputResolution.inputHeight}\n          min={100}\n          max={5000}\n          className=\"rounded-lg text-sm\"\n          onChange={(e) => {\n            setInputResolution({\n              ...inputResolution,\n              inputHeight: e.target.value,\n            })\n          }}\n        />\n        <Button\n          type=\"submit\"\n          variant=\"outline\"\n          className=\"rounded-lg px-3 text-sm\"\n        >\n          <ArrowRight size={18} />\n        </Button>\n      </form>\n\n      <h1 className=\"mb-3 mt-8 px-1 text-[0.85rem]\">Resolutions</h1>\n      <div className=\"flex flex-wrap gap-3\">\n        {resolutions.map((res, index) => (\n          <ResolutionButton\n            key={index}\n            resolutions={res?.resolutions}\n            name={res?.name}\n            icon={icons[res?.icon as keyof typeof icons]}\n            // variant={res.resolutions === resolution ? 'stylish' : 'outline'}\n            color={res.color}\n            variant=\"stylish\"\n            className=\"rounded-lg\"\n          />\n        ))}\n      </div>\n      <Separator className=\"mt-8 h-[0.1rem] w-full\" />\n\n      <CanvasRoundnessSlider />\n\n      <Separator className=\"mt-8 h-[0.1rem] w-full\" />\n\n      <h1 className=\"mb-3 mt-4 px-1 text-[0.85rem]\">Preview scale</h1>\n      <span className=\"inline-flex rounded-md shadow-sm\">\n        <button\n          type=\"button\"\n          className=\"relative inline-flex items-center rounded-l-md  px-2 py-2 ring-1 ring-inset ring-border focus:z-10 disabled:cursor-not-allowed bg-formDark text-dark\"\n          disabled={scrollScale === 1}\n          onClick={() => {\n            if (scrollScale === 1) return\n            setScrollScale(scrollScale + 0.1)\n          }}\n        >\n          <span className=\"sr-only\">Scale up</span>\n          <Plus className=\"h-5 w-5\" aria-hidden=\"true\" />\n        </button>\n        <button\n          type=\"button\"\n          className=\"relative -ml-px inline-flex items-center rounded-r-md bg-formDark px-2 py-2 text-dark ring-1 ring-inset ring-border focus:z-10 disabled:cursor-not-allowed\"\n          disabled={scrollScale <= 0.4}\n          onClick={() => {\n            if (scrollScale <= 0.4) return\n            setScrollScale(scrollScale - 0.1)\n          }}\n        >\n          <span className=\"sr-only\">Scale down</span>\n          <Minus className=\"h-5 w-5\" aria-hidden=\"true\" />\n        </button>\n      </span>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/canvas-options/resolution-button.tsx",
    "content": "import { useState } from 'react'\nimport { cn } from '@/utils/button-utils'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport { Button } from '@/components/ui/button'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { PopoverArrow } from '@radix-ui/react-popover'\nimport { ChevronDown } from 'lucide-react'\nimport { calculateEqualCanvasSize } from '@/utils/helper-fns'\n\nexport function ResolutionButton({\n  resolutions,\n  name,\n  icon,\n  className,\n  color,\n  variant,\n}: {\n  resolutions: any\n  name: string\n  icon?: React.ReactNode\n  className?: string\n  color?: string\n  variant: 'outline' | 'stylish'\n}) {\n  const [isHovering, setIsHovering] = useState(false)\n\n  const { setResolution, setScaleFactor, domResolution } = useResizeCanvas()\n  const { images, setImages, initialImageUploaded } = useImageOptions()\n  const { selectedImage } = useSelectedLayers()\n\n  const [domWidth]: number[] = domResolution.split('x').map(Number)\n\n  const handleMouseOver = () => {\n    setIsHovering(true)\n  }\n\n  const handleMouseOut = () => {\n    setIsHovering(false)\n  }\n\n  const calculateCanvasSize = (\n    imgWidth: number,\n    imgHeight: number,\n    padding: number\n  ) => {\n    const aspectRatio = imgWidth / imgHeight\n    let canvasWidth, canvasHeight\n\n    if (aspectRatio > 1) {\n      canvasWidth = imgWidth + 2 * padding\n      canvasHeight = canvasWidth / aspectRatio\n    } else {\n      canvasHeight = imgHeight + 2 * padding\n      canvasWidth = canvasHeight * aspectRatio\n    }\n\n    return `${canvasWidth}x${canvasHeight}`\n  }\n\n  if (name === 'Fit') {\n    return (\n      <Button\n        name=\"Fit\"\n        size=\"sm\"\n        className={cn('flex items-center gap-2 rounded-lg', className)}\n        variant={variant}\n        onClick={() => {\n          if (images.length === 0) return\n\n          const padding = 200\n          const img = new Image()\n          img.src = images[0].image\n\n          img.onload = () => {\n            const { naturalWidth, naturalHeight } = img\n            const newResolution = calculateCanvasSize(\n              naturalWidth,\n              naturalHeight,\n              padding\n            )\n            setResolution(newResolution.toString())\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          imageSize: '0.75',\n                        },\n                      }\n                    : image\n                )\n              )\n          }\n        }}\n        aria-label={name}\n      >\n        Fit image\n      </Button>\n    )\n  }\n\n  if (name === 'Equal padding') {\n    return (\n      <Button\n        name=\"Equal padding\"\n        size=\"sm\"\n        className={cn('flex items-center gap-2 rounded-lg', className)}\n        variant={variant}\n        onClick={() => {\n          if (images.length === 0) return\n\n          const padding = 250\n          const img = new Image()\n          img.src = images[0].image\n\n          img.onload = () => {\n            const { naturalWidth, naturalHeight } = img\n            const newResolution = calculateEqualCanvasSize(\n              naturalWidth,\n              naturalHeight,\n              padding\n            )\n            setResolution(newResolution.toString())\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          imageSize: '0.75',\n                        },\n                      }\n                    : image\n                )\n              )\n          }\n        }}\n        aria-label={name}\n      >\n        Equal padding\n      </Button>\n    )\n  }\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button\n          onMouseOver={handleMouseOver}\n          onMouseOut={handleMouseOut}\n          className={cn(\n            'flex items-center gap-1.5 rounded-lg transition-colors',\n            className\n          )}\n          style={{\n            backgroundColor: isHovering ? `${color}1A` : '',\n            color: isHovering ? `${color}` : '',\n            borderColor: isHovering ? `${color}33` : '',\n          }}\n          variant={variant}\n          size=\"sm\"\n        >\n          {icon && <div>{icon}</div>}\n          <div className=\"sr-only\">{name}</div>\n          <ChevronDown size={18} className=\"translate-y-[1.5px] text-inherit\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        avoidCollisions\n        className=\"grid w-[220px] grid-cols-1 gap-3\"\n      >\n        <PopoverArrow\n          width={14}\n          height={7}\n          className=\"stroke fill-[#1A1C1F] stroke-border\"\n        />\n        {resolutions?.map((res: { resolution: string; preset: string }) => {\n          return (\n            <Button\n              onClick={() => {\n                const [outputWidth]: number[] = res.resolution\n                  .split('x')\n                  .map(Number)\n\n                if (!initialImageUploaded) return\n\n                setResolution(res.resolution)\n\n                setScaleFactor(outputWidth / domWidth)\n              }}\n              variant=\"stylish\"\n              size={'sm'}\n              style={{\n                backgroundColor: `${color}1A`,\n                color: `${color}`,\n                borderColor: `${color}33`,\n              }}\n              key={res.resolution}\n            >\n              <p>{`${res.preset}`}</p>\n              &nbsp;\n              <p>({res.resolution})</p>\n            </Button>\n          )\n        })}\n      </PopoverContent>\n    </Popover>\n  )\n}\n"
  },
  {
    "path": "components/editor/frame-options/additional-frame-options.tsx",
    "content": "import PopupColorPicker from '@/components/popup-color-picker'\nimport { Switch } from '@/components/ui/switch'\nimport { useFrameOptions } from '@/store/use-frame-options'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { Settings2 } from 'lucide-react'\n\nexport default function AdditionalFrameOptions() {\n  const {\n    setShowSearchBar,\n    setShowStroke,\n    macOsDarkColor,\n    setMacOsDarkColor,\n    setMacOsLightColor,\n    macOsLightColor,\n    setArcDarkMode,\n    showStroke,\n    arcDarkMode,\n    hideButtons,\n    setHideButtons,\n    hasButtonColor,\n    setHasButtonColor,\n  } = useFrameOptions()\n  const { selectedImage } = useSelectedLayers()\n  const { images } = useImageOptions()\n\n  const browserFrame = selectedImage ? images[selectedImage - 1]?.frame : 'None'\n\n  if (browserFrame !== 'None')\n    return (\n      <div\n        className={`${selectedImage ? '' : 'pointer-events-none opacity-40'} `}\n      >\n        <h3 className=\"mb-6 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n          <Settings2 size={20} />\n          <span>Additional options</span>\n        </h3>\n\n        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (\n          <div className=\"mb-6 flex items-center justify-between gap-4 px-1\">\n            <h1 className=\"text-[0.85rem]\">Show searchbar :</h1>\n            <Switch\n              defaultChecked={false}\n              onCheckedChange={(checked) => {\n                setShowSearchBar(checked)\n              }}\n            />\n          </div>\n        )}\n\n        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (\n          <div className=\"mb-6 flex items-center justify-between gap-4 px-1\">\n            <h1 className=\"text-[0.85rem]\">Colorful buttons :</h1>\n            <Switch\n              defaultChecked={true}\n              checked={hasButtonColor}\n              onCheckedChange={(checked) => {\n                setHasButtonColor(checked)\n              }}\n            />\n          </div>\n        )}\n\n        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (\n          <div className=\"mb-6 flex items-center justify-between gap-4 px-1\">\n            <h1 className=\"text-[0.85rem]\">Hide buttons :</h1>\n            <Switch\n              defaultChecked={false}\n              checked={hideButtons}\n              onCheckedChange={(checked) => {\n                setHideButtons(checked)\n              }}\n            />\n          </div>\n        )}\n\n        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (\n          <div className=\"mb-6 flex items-center justify-between gap-4 px-1\">\n            <h1 className=\"text-[0.85rem]\">Frame color :</h1>\n            <PopupColorPicker\n              shouldShowDropdown={false}\n              shouldShowAlpha={true}\n              color={\n                browserFrame === 'MacOS Dark' ? macOsDarkColor : macOsLightColor\n              }\n              onChange={(color) => {\n                if (browserFrame === 'MacOS Dark') {\n                  setMacOsDarkColor(color)\n                } else {\n                  setMacOsLightColor(color)\n                }\n              }}\n            />\n          </div>\n        )}\n\n        {browserFrame === 'Shadow' && (\n          <div className=\"mb-6 flex items-center gap-4 px-1 md:max-w-full\">\n            <h1 className=\"text-[0.85rem]\">Show outline :</h1>\n            <Switch\n              defaultChecked={true}\n              checked={showStroke}\n              onCheckedChange={(checked) => {\n                setShowStroke(checked)\n              }}\n            />\n          </div>\n        )}\n\n        {browserFrame === 'Arc' && (\n          <div className=\"mb-6 flex items-center gap-4 px-1 md:max-w-full\">\n            <h1 className=\"text-[0.85rem]\">Dark mode :</h1>\n            <Switch\n              defaultChecked={false}\n              checked={arcDarkMode}\n              onCheckedChange={(checked) => {\n                setArcDarkMode(checked)\n              }}\n            />\n          </div>\n        )}\n      </div>\n    )\n}\n"
  },
  {
    "path": "components/editor/frame-options/frame-picker.tsx",
    "content": "import {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { FrameTypes, useFrameOptions } from '@/store/use-frame-options'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport { cn } from '@/utils/button-utils'\n\nexport default function FramePicker() {\n  const { setFrameHeight, frameHeight } = useFrameOptions()\n  const { selectedImage } = useSelectedLayers()\n  const { setImages, images } = useImageOptions()\n  const { setShowControls } = useMoveable()\n\n  const frameChangeHandler = (frame: FrameTypes) => {\n    selectedImage &&\n      setImages(\n        images.map((image, index) =>\n          index === selectedImage - 1\n            ? {\n                ...image,\n                frame,\n                style: {\n                  ...image.style,\n                  imageRoundness:\n                    frame === 'None' ? 0.4 : frame === 'Arc' ? 1.5 : 0.7,\n                },\n              }\n            : image\n        )\n      )\n    setShowControls(false)\n  }\n\n  return (\n    <>\n      <div\n        className={`mb-3 mt-4 flex items-center px-1 md:max-w-full \n        ${selectedImage ? '' : 'pointer-events-none opacity-40'}\n      `}\n      >\n        <h1 className=\"text-[0.85rem]\">Browser frames:</h1>\n      </div>\n\n      <div className=\"mt-2 grid w-full grid-cols-3 flex-wrap gap-x-2.5 gap-y-6\">\n        <FrameContainer\n          text=\"None\"\n          onClick={() => {\n            frameChangeHandler('None')\n          }}\n        >\n          <div className=\"flex h-full w-full flex-col justify-center overflow-hidden rounded-sm\">\n            <div className=\"w-full flex-1 bg-primary/80\" />\n          </div>\n        </FrameContainer>\n\n        <FrameContainer\n          text=\"MacOS Dark\"\n          onClick={() => {\n            frameChangeHandler('MacOS Dark')\n          }}\n        >\n          <div className=\"flex h-full w-full flex-col justify-center overflow-hidden rounded-sm\">\n            <div className=\"flex w-full basis-[30%] bg-[#454545] shadow-sm\">\n              <div className={`flex-center basis-[50%] gap-0.5 `}>\n                <div className=\"h-1 w-1 rounded-full bg-[#f7645ccc]\" />\n                <div className=\"h-1 w-1 rounded-full bg-[#fbc341d2]\" />\n                <div className=\"h-1 w-1 rounded-full bg-[#3cc84ac5]\" />\n              </div>\n            </div>\n            <div className=\"w-full flex-1 bg-primary/80\" />\n          </div>\n        </FrameContainer>\n\n        <FrameContainer\n          text=\"MacOS Light\"\n          onClick={() => {\n            frameChangeHandler('MacOS Light')\n          }}\n        >\n          <div className=\"flex h-full w-full flex-col justify-center overflow-hidden rounded-sm\">\n            <div className=\"flex w-full basis-[30%] bg-[#E3E2E3] shadow-sm\">\n              <div className={`flex-center basis-[50%] gap-0.5 `}>\n                <div className=\"h-1 w-1 rounded-full bg-[#f7645ccc]\" />\n                <div className=\"h-1 w-1 rounded-full bg-[#fbc341d2]\" />\n                <div className=\"h-1 w-1 rounded-full bg-[#3cc84ac5]\" />\n              </div>\n            </div>\n            <div className=\"w-full flex-1 bg-primary/80\" />\n          </div>\n        </FrameContainer>\n\n        <FrameContainer\n          text=\"Arc\"\n          onClick={() => {\n            frameChangeHandler('Arc')\n          }}\n        >\n          <div className=\"flex-center h-[4.5rem] w-24 flex-col rounded-sm border border-[#fff]/20 bg-[#fff]/20 p-1 shadow-xl\">\n            <div className=\"h-full w-full rounded-[2px] bg-primary shadow-sm\" />\n          </div>\n        </FrameContainer>\n\n        <FrameContainer\n          text=\"Shadow\"\n          onClick={() => {\n            frameChangeHandler('Shadow')\n          }}\n          className=\"translate-y-2\"\n        >\n          <div className=\"flex-center h-[4.5rem] w-24 flex-col rounded-sm\">\n            <div className=\"h-full w-full rounded-[2px] bg-primary/80\" />\n          </div>\n        </FrameContainer>\n      </div>\n\n      {selectedImage &&\n        images[selectedImage - 1]?.frame !== 'Shadow' &&\n        images[selectedImage - 1]?.frame !== 'None' && (\n          <div\n            className={`mt-8 flex flex-col gap-3 px-1 md:max-w-full ${\n              selectedImage ? '' : 'pointer-events-none opacity-40'\n            }`}\n          >\n            <h1 className=\"text-[0.85rem]\">Frame size</h1>\n            <Select\n              defaultValue={frameHeight}\n              onValueChange={(value) => setFrameHeight(value)}\n            >\n              <SelectTrigger className=\"w-[7rem]\">\n                <SelectValue placeholder=\"Medium\" />\n              </SelectTrigger>\n              <SelectContent className=\"w-[7rem]\">\n                <SelectItem value=\"small\">Small</SelectItem>\n                <SelectItem value=\"medium\">Medium</SelectItem>\n                <SelectItem value=\"large\">Large</SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n        )}\n    </>\n  )\n}\n\nexport function FrameContainer({\n  children,\n  text,\n  onClick,\n  className,\n}: {\n  children: React.ReactNode\n  text: FrameTypes\n  onClick?: () => void\n  className?: string\n}) {\n  const { selectedImage } = useSelectedLayers()\n  const { images } = useImageOptions()\n\n  return (\n    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>\n      <button\n        onClick={onClick}\n        className={`relative h-[3.55rem] w-[4.6rem] overflow-hidden whitespace-nowrap rounded-lg border border-border/80 bg-gray-300 ring-offset-background transition-colors focus:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ${\n          selectedImage && images[selectedImage - 1]?.frame === text\n            ? 'ring-2 ring-ring ring-offset-2'\n            : ''\n        }`}\n      >\n        <div\n          className={cn(\n            'absolute bottom-0 h-2/3 w-2/3 translate-x-1/2 translate-y-1 scale-150  overflow-hidden rounded-sm',\n            className\n          )}\n          style={{\n            boxShadow: `0px 10px 40px #000${\n              text === 'Shadow' ? ',-4px -3.5px rgba(0,0,0,0.8)' : ''\n            }`,\n          }}\n        >\n          {children}\n        </div>\n      </button>\n\n      <p className=\"mt-2 text-center text-[0.75rem] font-medium text-dark\">\n        {text.replace('MacOS', 'Mac')}\n      </p>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/frame-options/index.tsx",
    "content": "import RoundnessOption from '../image-options/roundness-option'\nimport FramePicker from './frame-picker'\nimport AdditionalFrameOptions from './additional-frame-options'\nimport { Separator } from '@/components/ui/separator'\n\nexport default function FrameOptions() {\n  return (\n    <>\n      <FramePicker />\n\n      <RoundnessOption />\n\n      <Separator className=\"mt-8 h-[0.1rem] w-full\" />\n\n      <AdditionalFrameOptions />\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/image-context-menu.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuTrigger,\n} from '@/components/ui/context-menu'\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '@/components/ui/dialog'\nimport { useColorExtractor } from '@/store/use-color-extractor'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport {\n  BringToFront,\n  CropIcon,\n  ImagePlus,\n  SendToBack,\n  Trash,\n  Wand,\n} from 'lucide-react'\nimport dynamic from 'next/dynamic'\nimport React, { ChangeEvent, useEffect, useRef, useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { type Crop } from 'react-image-crop'\nimport 'react-image-crop/dist/ReactCrop.css'\nimport Loader from '../loader'\n\nconst DynamicCropComponent = dynamic(() =>\n  import('react-image-crop').then((mod) => mod.ReactCrop)\n)\n\nexport default function ContextMenuImage({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  const [crop, setCrop] = useState<Crop>({\n    unit: '%', // Can be 'px' or '%'\n    x: 25,\n    y: 25,\n    width: 50,\n    height: 50,\n  })\n  const imgRef = useRef<HTMLImageElement>(null)\n  const { setImages, images } = useImageOptions()\n  const [isRemovingBackground, setIsRemovingBackground] = useState(false)\n  const [isBgRemovalDialogOpen, setIsBgRemovalDialogOpen] = useState(false)\n  const [isProcessingBackground, setIsProcessingBackground] = useState(false)\n  const [bgRemovalError, setBgRemovalError] = useState<string | null>(null)\n  const [processedImageUrl, setProcessedImageUrl] = useState<string | null>(\n    null\n  )\n  const { selectedImage, setSelectedImage, setEnableCrop, enableCrop } =\n    useSelectedLayers()\n  const { showControls, setShowControls } = useMoveable()\n  const workerRef = useRef<Worker | null>(null)\n\n  const handleImageDelete = (id: number) => {\n    if (images.length === 1) {\n      setImages([])\n      return\n    }\n\n    if (selectedImage) {\n      setImages(\n        images.map((image, index) =>\n          index === selectedImage - 1\n            ? {\n                ...image,\n                image: '',\n              }\n            : image\n        )\n      )\n    }\n\n    setSelectedImage(null)\n  }\n\n  const bringToFrontOrBack = (direction: 'front' | 'back') => {\n    if (selectedImage) {\n      setImages(\n        images.map((image, index) =>\n          index === selectedImage - 1\n            ? {\n                ...image,\n                style: {\n                  ...image.style,\n                  zIndex:\n                    direction === 'front'\n                      ? image.style.zIndex + 1\n                      : image.style.zIndex - 1,\n                },\n              }\n            : image\n        )\n      )\n    }\n  }\n\n  useEffect(() => {\n    // Terminate existing worker if component re-renders or dependencies change\n    workerRef.current?.terminate()\n\n    if (isBgRemovalDialogOpen && selectedImage) {\n      const currentImage = images[selectedImage - 1]\n      if (!currentImage || !currentImage.image) return\n\n      setProcessedImageUrl(null) // Reset image URL first\n      setBgRemovalError(null) // Reset error\n      setIsProcessingBackground(true)\n\n      // Create and configure the worker\n      workerRef.current = new Worker(\n        new URL('@/workers/background-removal.worker.ts', import.meta.url)\n      )\n\n      workerRef.current.onmessage = (\n        event: MessageEvent<\n          { type: 'success'; url: string } | { type: 'error'; error: string }\n        >\n      ) => {\n        if (event.data.type === 'success') {\n          setProcessedImageUrl(event.data.url)\n          setBgRemovalError(null)\n        } else if (event.data.type === 'error') {\n          console.error('Background removal worker error:', event.data.error)\n          setBgRemovalError(event.data.error)\n          // Keep dialog open to show error or close?\n          // setIsBgRemovalDialogOpen(false);\n        }\n        setIsProcessingBackground(false)\n      }\n\n      workerRef.current.onerror = (error) => {\n        console.error('Unhandled worker error:', error)\n        setBgRemovalError('An unexpected worker error occurred.')\n        setIsProcessingBackground(false)\n        // setIsBgRemovalDialogOpen(false);\n      }\n\n      // Send image source to worker\n      workerRef.current.postMessage({ src: currentImage.image })\n    } else {\n      // If dialog is closed, ensure worker is terminated\n      workerRef.current?.terminate()\n      workerRef.current = null\n    }\n\n    // Cleanup function to terminate worker on unmount or when dialog closes\n    return () => {\n      workerRef.current?.terminate()\n      workerRef.current = null\n    }\n    // Rerun effect when dialog opens/closes or selected image changes\n  }, [isBgRemovalDialogOpen, selectedImage, images]) // Added images dependency\n\n  useHotkeys(['Delete', 'Backspace'], () => {\n    if (selectedImage)\n      if (showControls) {\n        handleImageDelete(selectedImage)\n        setShowControls(false)\n        setSelectedImage(null)\n      }\n  })\n\n  const cropImageNow = () => {\n    const canvas = document.createElement('canvas')\n    const image = imgRef.current\n    if (!image) return\n    const scaleX = image.naturalWidth / image.width\n    const scaleY = image.naturalHeight / image.height\n    canvas.width = crop.width\n    canvas.height = crop.height\n    const ctx: any = canvas.getContext('2d')\n\n    const pixelRatio = window.devicePixelRatio\n    canvas.width = crop.width * pixelRatio * scaleX\n    canvas.height = crop.height * pixelRatio * scaleY\n    ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)\n    ctx.imageSmoothingQuality = 'high'\n\n    ctx.drawImage(\n      image,\n      crop.x * scaleX,\n      crop.y * scaleY,\n      crop.width * scaleX,\n      crop.height * scaleY,\n      0,\n      0,\n      crop.width * scaleX,\n      crop.height * scaleY\n    )\n\n    const base64Image = canvas.toDataURL('image/png')\n    selectedImage &&\n      setImages(\n        images.map((image, index) =>\n          index === selectedImage - 1\n            ? {\n                ...image,\n                image: base64Image,\n              }\n            : image\n        )\n      )\n  }\n\n  return (\n    <Dialog\n      open={enableCrop}\n      onOpenChange={(open) => {\n        if (open === false) setEnableCrop(false)\n        setEnableCrop(open)\n      }}\n    >\n      <ContextMenu>\n        <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>\n        <ContextMenuContent className=\"w-64\">\n          <ContextMenuItem\n            inset\n            onClick={() => {\n              bringToFrontOrBack('back')\n            }}\n            // disabled={\n            //   !selectedImage || images[selectedImage - 1].style.zIndex === 2\n            // }\n          >\n            Send back\n            <ContextMenuShortcut>\n              <BringToFront size={19} className=\"opacity-80\" />\n            </ContextMenuShortcut>\n          </ContextMenuItem>\n          <ContextMenuItem\n            inset\n            onClick={() => {\n              bringToFrontOrBack('front')\n            }}\n          >\n            Bring forward\n            <ContextMenuShortcut>\n              <SendToBack size={19} className=\"opacity-80\" />\n            </ContextMenuShortcut>\n          </ContextMenuItem>\n\n          <ContextMenuSeparator />\n\n          <ReplaceImage />\n\n          <div className=\"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\">\n            <div\n              onClick={() => setIsBgRemovalDialogOpen(true)}\n              className=\"ml-6 cursor-pointer\"\n            >\n              Remove background\n            </div>\n            <span className=\"ml-auto text-xs tracking-widest text-muted-foreground\">\n              <Wand size={19} className=\"opacity-80\" />\n            </span>\n          </div>\n\n          <DialogTrigger asChild>\n            <div className=\"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\">\n              <div className=\"ml-6 cursor-pointer\">Crop</div>\n              <span className=\"ml-auto text-xs tracking-widest text-muted-foreground\">\n                <CropIcon size={19} className=\"opacity-80\" />\n              </span>\n            </div>\n          </DialogTrigger>\n\n          {/* <ContextMenuSub>\n            <ContextMenuSubTrigger inset>More Tools</ContextMenuSubTrigger>\n            <ContextMenuSubContent className=\"w-48\">\n            </ContextMenuSubContent>\n          </ContextMenuSub> */}\n\n          <ContextMenuSeparator />\n          <ContextMenuItem\n            inset\n            onClick={() => {\n              selectedImage && handleImageDelete(selectedImage)\n            }}\n            className=\"text-[#F46567]/70 focus:text-[#f46567]/80\"\n          >\n            Delete\n            <ContextMenuShortcut>\n              <Trash size={19} className=\"text-[#F46567]/70 opacity-80\" />\n            </ContextMenuShortcut>\n          </ContextMenuItem>\n        </ContextMenuContent>\n      </ContextMenu>\n      <DialogContent className=\"flex h-fit max-h-[95vh]  w-1/2 flex-col gap-4\">\n        <DialogHeader className=\"mb-4\">\n          <DialogTitle>Crop image</DialogTitle>\n        </DialogHeader>\n\n        <div className=\"mb-4 h-full w-full flex-1 overflow-hidden overflow-y-auto\">\n          {selectedImage && (\n            <DynamicCropComponent\n              crop={crop}\n              onChange={(c) => setCrop(c)}\n              disabled={!enableCrop || !selectedImage}\n              onComplete={(c) => {\n                console.log(c)\n              }}\n            >\n              {/* eslint-disable-next-line @next/next/no-img-element */}\n              <img\n                ref={imgRef}\n                src={images[selectedImage - 1].image}\n                alt=\"Crop selected image\"\n                className=\"h-full w-full object-cover\"\n              />\n            </DynamicCropComponent>\n          )}\n        </div>\n\n        <DialogFooter className=\"mt-auto flex items-center gap-1.5\">\n          <Button\n            variant=\"outline\"\n            onClick={() => {\n              setEnableCrop(false)\n            }}\n          >\n            Cancel\n          </Button>\n\n          <Button\n            onClick={() => {\n              setEnableCrop(false)\n              cropImageNow()\n            }}\n            className=\"flex-center gap-1.5\"\n          >\n            <span>Done</span>\n            <CropIcon size={19} className=\"opacity-80\" />\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n\n      <Dialog\n        open={isBgRemovalDialogOpen}\n        onOpenChange={setIsBgRemovalDialogOpen}\n      >\n        <DialogContent className=\"flex h-fit max-h-[95vh] w-1/2 flex-col gap-4\">\n          <DialogHeader>\n            <DialogTitle className=\"mb-4 flex items-center gap-1.5\">\n              <Wand size={19} className=\"opacity-80\" />\n              <span>Remove Background</span>\n            </DialogTitle>\n          </DialogHeader>\n\n          <div\n            className=\"relative mb-4 flex h-full w-full flex-1 items-center justify-center overflow-hidden overflow-y-auto\"\n            style={{\n              // Apply checkered background only when done processing, successful, and image is ready\n              backgroundImage:\n                !isProcessingBackground && processedImageUrl && !bgRemovalError\n                  ? 'linear-gradient(45deg, rgba(204,204,204,0.05) 25%, transparent 25%), linear-gradient(-45deg, rgba(204,204,204,0.05) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(204,204,204,0.05) 75%), linear-gradient(-45deg, transparent 75%, rgba(204,204,204,0.05) 75%)'\n                  : 'none',\n              backgroundSize:\n                !isProcessingBackground && processedImageUrl && !bgRemovalError\n                  ? '20px 20px'\n                  : 'auto',\n              backgroundPosition:\n                !isProcessingBackground && processedImageUrl && !bgRemovalError\n                  ? '0 0, 0 10px, 10px -10px, -10px 0px'\n                  : 'initial',\n              // Keep a fallback bg or use theme background\n              backgroundColor:\n                !isProcessingBackground && processedImageUrl && !bgRemovalError\n                  ? 'hsl(var(--background))' // Use theme background\n                  : 'hsl(var(--muted) / 0.1)', // Original dim background\n            }}\n          >\n            {isProcessingBackground && (\n              <div className=\"absolute inset-0 z-10 flex flex-col items-center justify-center bg-black/60 \">\n                <Loader />\n              </div>\n            )}\n            {/* Display Error Message */}\n            {!isProcessingBackground && bgRemovalError && (\n              <div className=\"absolute inset-0 z-10 flex flex-col items-center justify-center bg-destructive/60 p-4 text-center\">\n                <p className=\"font-semibold text-white\">\n                  Error Removing Background\n                </p>\n                <p className=\"mt-1 text-xs text-white/80\">{bgRemovalError}</p>\n              </div>\n            )}\n            {selectedImage && !processedImageUrl && (\n              <img\n                src={images[selectedImage - 1]?.image}\n                alt=\"Original image\"\n                className=\"h-full w-full object-cover\"\n                // Dim image slightly if there's an error overlay\n                style={{ opacity: bgRemovalError ? 0.5 : 1 }}\n              />\n            )}\n            {processedImageUrl && (\n              <img\n                src={processedImageUrl}\n                alt=\"Image with background removed\"\n                className=\"h-full w-full object-cover\" // Show processed image even if error occurred previously\n              />\n            )}\n          </div>\n\n          <DialogFooter className=\"mt-auto flex items-center gap-1.5\">\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                setIsBgRemovalDialogOpen(false)\n                setProcessedImageUrl(null)\n                setBgRemovalError(null)\n                setIsProcessingBackground(false) // Ensure loading state is reset\n              }}\n            >\n              Cancel\n            </Button>\n\n            <Button\n              onClick={() => {\n                if (selectedImage && processedImageUrl) {\n                  setImages(\n                    images.map((img, index) =>\n                      index === selectedImage - 1\n                        ? {\n                            ...img,\n                            image: processedImageUrl,\n                            style: {\n                              ...img.style,\n                              shadowName: 'None',\n                              imageShadow: '0 0 0 0',\n                            },\n                          }\n                        : img\n                    )\n                  )\n                }\n                setIsBgRemovalDialogOpen(false)\n                setProcessedImageUrl(null)\n              }}\n              disabled={\n                isProcessingBackground || !processedImageUrl || !!bgRemovalError\n              } // Disable if processing, no result, or error\n              className=\"flex-center gap-1.5\"\n            >\n              Done\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </Dialog>\n  )\n}\n\nfunction ReplaceImage() {\n  const { setImages, images } = useImageOptions()\n  const { selectedImage, setSelectedImage } = useSelectedLayers()\n\n  const { setImagesCheck, imagesCheck } = useColorExtractor()\n\n  const onDrop = async (file: any) => {\n    const analyze = (await import('rgbaster')).default\n    if (file) {\n      const imageUrl = URL.createObjectURL(file)\n\n      const result = await analyze(imageUrl, {\n        scale: 0.3,\n      })\n      const extractedColors = result.slice(0, 12)\n\n      selectedImage &&\n        setImages(\n          images.map((image, index) =>\n            index === selectedImage - 1\n              ? {\n                  ...image,\n                  image: imageUrl,\n                  colors: extractedColors,\n                }\n              : image\n          )\n        )\n\n      setImagesCheck([...imagesCheck, imageUrl])\n    }\n  }\n  return (\n    <div className=\"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\">\n      <label className=\"ml-6\" htmlFor=\"file-replace\">\n        Replace image\n      </label>\n      <input\n        id=\"file-replace\"\n        name=\"file-replace\"\n        type=\"file\"\n        onChange={(e: ChangeEvent<HTMLInputElement>) => {\n          onDrop(e.target.files?.[0])\n        }}\n        accept=\"image/*\"\n        className=\"sr-only\"\n      />\n      <span className=\"ml-auto text-xs tracking-widest text-muted-foreground\">\n        <ImagePlus size={19} className=\"opacity-80\" />\n      </span>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/image-options/add-image-button.tsx",
    "content": "import { ChangeEvent, useRef, useState } from 'react'\nimport { X, Plus, Upload } from 'lucide-react'\nimport { useImageOptions } from '@/store/use-image-options'\nimport { useColorExtractor } from '@/store/use-color-extractor'\nimport { calculateEqualCanvasSize } from '@/utils/helper-fns'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport Dropzone from 'react-dropzone'\n\ntype AddImageButtonProps = {}\n\nexport default function AddImageButton({}: AddImageButtonProps) {\n  const { setImages, images, defaultStyle } = useImageOptions()\n  const { imagesCheck, setImagesCheck } = useColorExtractor()\n  const uploadRef = useRef<HTMLInputElement>(null)\n  const { automaticResolution, setResolution } = useResizeCanvas()\n  const [isDragging, setIsDragging] = useState<boolean>(false)\n\n  const handleImageUpload = (file: File) => {\n    const imageUrl = URL.createObjectURL(file)\n    setImages([\n      ...images,\n      {\n        image: imageUrl,\n        id: images.length + 1,\n        style:\n          images.length < 1\n            ? defaultStyle\n            : {\n                ...defaultStyle,\n                imageSize: '0.5',\n              },\n      },\n    ])\n    setImagesCheck([...imagesCheck, imageUrl])\n\n    if (images.length > 0) return\n    if (automaticResolution) {\n      const padding = 200\n      const img = new Image()\n      img.src = imageUrl\n\n      img.onload = () => {\n        const { naturalWidth, naturalHeight } = img\n        const newResolution = calculateEqualCanvasSize(\n          naturalWidth,\n          naturalHeight,\n          padding\n        )\n        setResolution(newResolution.toString())\n      }\n    }\n  }\n\n  const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0]\n    if (file) {\n      handleImageUpload(file)\n    }\n  }\n\n  const handleDrop = (acceptedFiles: File[]) => {\n    if (acceptedFiles.length > 0) {\n      handleImageUpload(acceptedFiles[0])\n    }\n    setIsDragging(false)\n  }\n\n  return (\n    <div className=\"mb-4 mt-2 h-[8rem] w-full px-1 text-sm\">\n      <Dropzone\n        multiple={false}\n        onDrop={handleDrop}\n        onDragEnter={() => setIsDragging(true)}\n        onDragLeave={() => setIsDragging(false)}\n        accept={{ 'image/*': [] }}\n        noClick\n      >\n        {({ getRootProps, getInputProps, open }) => (\n          <div\n            {...getRootProps()}\n            className={`relative flex h-full w-full flex-col rounded-xl border-2 p-1 transition-all duration-300 ${\n              isDragging\n                ? 'scale-[1.02] border-[#898aeb] bg-[#898aeb]/10'\n                : 'border-[#898aeb]/20 hover:border-[#898aeb]/60'\n            }`}\n          >\n            <div\n              className=\"group relative flex h-full w-full cursor-pointer items-center justify-center overflow-hidden rounded-lg\"\n              onClick={open}\n              tabIndex={0}\n              onKeyDown={(e) => {\n                if (e.key === 'Enter' || e.key === ' ') {\n                  e.preventDefault()\n                  open()\n                }\n              }}\n            >\n              {isDragging ? (\n                <div className=\"absolute inset-0 flex items-center justify-center rounded-lg bg-gradient-to-br from-[#898aeb]/20 to-[#d8b9e3]/20\">\n                  <div className=\"flex flex-col items-center\">\n                    <Upload\n                      className=\"mb-2 animate-bounce text-[#898aeb]\" \n                      size={24}\n                    />\n                    <span className=\"text-sm font-medium text-[#898aeb]\">\n                      Drop here\n                    </span>\n                  </div>\n                </div>\n              ) : (\n                <div className=\"flex flex-col items-center\">\n                  <Plus\n                    className=\"mb-2 cursor-pointer text-purple/60 transition-transform focus:ring-1 group-hover:scale-110 group-hover:text-purple/80\"\n                    size={26}\n                  />\n                  <span className=\"text-sm font-medium text-purple/60\">\n                    Click or drag image\n                  </span>\n                </div>\n              )}\n            </div>\n            <input\n              {...getInputProps()}\n            />\n          </div>\n        )}\n      </Dropzone>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/image-options/index.tsx",
    "content": "'use client'\n\nimport { Focus, GalleryVerticalEnd } from 'lucide-react'\nimport SizeOption from './scale-options'\nimport RoundnessOption from './roundness-option'\nimport InsetOption from './inset-option'\n\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from '@/components/ui/accordion'\nimport { useImageOptions } from '@/store/use-image-options'\nimport AddImageButton from './add-image-button'\nimport ShadowSettings from './shadow-settings'\n\nexport default function ImageOptions() {\n  const { accordionOpen, setAccordionOpen } = useImageOptions()\n\n  return (\n    <>\n      <AddImageButton />\n      <Accordion\n        type=\"single\"\n        collapsible\n        defaultValue={accordionOpen.appearanceOpen ? 'appearance' : ''}\n        className=\"mt-4 w-full\"\n      >\n        <AccordionItem value=\"appearance\">\n          <AccordionTrigger\n            onClick={() =>\n              setAccordionOpen({\n                ...accordionOpen,\n                appearanceOpen: !accordionOpen.appearanceOpen,\n              })\n            }\n          >\n            <h3 className=\"flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n              <Focus size={20} />\n              <span>Appearance</span>\n            </h3>\n          </AccordionTrigger>\n          <AccordionContent>\n            <SizeOption />\n            <InsetOption />\n            <RoundnessOption />\n          </AccordionContent>\n        </AccordionItem>\n      </Accordion>\n      <Accordion\n        type=\"single\"\n        collapsible\n        defaultValue={accordionOpen.shadowOpen ? 'shadow' : ''}\n        className=\"mt-2 w-full\"\n      >\n        <AccordionItem value=\"shadow\">\n          <AccordionTrigger\n            onClick={() =>\n              setAccordionOpen({\n                ...accordionOpen,\n                shadowOpen: !accordionOpen.shadowOpen,\n              })\n            }\n          >\n            <h3 className=\"flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n              <GalleryVerticalEnd size={20} className=\"rotate-90\" />\n              <span>Shadow</span>\n            </h3>\n          </AccordionTrigger>\n          <AccordionContent>\n            <ShadowSettings />\n          </AccordionContent>\n        </AccordionItem>\n      </Accordion>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/image-options/inset-option.tsx",
    "content": "'use client'\n\nimport { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { Button } from '@/components/ui/button'\n\nexport default function InsetOption() {\n  const { images, updateImageStyle, getImage } = useImageOptions()\n  const { setShowControls, showControls } = useMoveable()\n  const { selectedImage } = useSelectedLayers()\n\n  return (\n    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>\n      <div className=\"mb-3 mt-6 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Inset</h1>\n        {images.length !== 0 && (\n          <Popover>\n            <PopoverTrigger asChild>\n              <Button\n                className=\"ml-2 h-5 w-5 rounded-md\"\n                style={{\n                  backgroundColor: selectedImage\n                    ? getImage(selectedImage)?.style.insetColor\n                    : '#fff',\n                }}\n                variant=\"outline\"\n              />\n            </PopoverTrigger>\n            <PopoverContent\n              align=\"start\"\n              className=\"w-[250px] rounded-lg bg-formDark p-4\"\n            >\n              <div className=\"grid gap-4\">\n                <div className=\"space-y-2\">\n                  <h4 className=\"pb-1 text-sm font-medium tracking-tight\">\n                    Detected colors\n                  </h4>\n                  <hr className=\"border-border pt-2\" />\n                  <div className=\"flex flex-wrap gap-2\">\n                    {images[selectedImage! - 1]?.extractedColors?.map(\n                      (color) => (\n                        <button\n                          key={color.color}\n                          className={`h-7 w-7 rounded-sm ${\n                            images[selectedImage! - 1]?.style.insetColor ===\n                            color.color\n                              ? 'ring-2 ring-ring ring-offset-2'\n                              : ''\n                          }`}\n                          style={{ backgroundColor: color.color }}\n                          onClick={() => {\n                            selectedImage &&\n                              updateImageStyle(selectedImage, {\n                                insetColor: color.color,\n                              })\n                          }}\n                        />\n                      )\n                    )}\n                  </div>\n                </div>\n              </div>\n            </PopoverContent>\n          </Popover>\n        )}\n      </div>\n\n      <div className=\"flex gap-4 px-1 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={150}\n          min={0}\n          step={0.02}\n          onValueChange={(value: number[]) => {\n            setShowControls(false)\n            selectedImage &&\n              updateImageStyle(selectedImage, {\n                insetSize: value[0].toString(),\n              })\n          }}\n          value={\n            images.length !== 0 && selectedImage\n              ? [+(getImage(selectedImage)?.style.insetSize ?? 0)]\n              : [10]\n          }\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            setShowControls(false)\n            selectedImage &&\n              updateImageStyle(selectedImage, {\n                insetSize:\n                  +(getImage(selectedImage)?.style.insetSize ?? 0) <= 149\n                    ? (\n                        +(getImage(selectedImage)?.style.insetSize ?? 0) + 4\n                      ).toString()\n                    : '150',\n              })\n          }}\n          onDecrement={() => {\n            setShowControls(false)\n            selectedImage &&\n              updateImageStyle(selectedImage, {\n                insetSize:\n                  +(getImage(selectedImage)?.style.insetSize ?? 0) >= 0\n                    ? (\n                        +(getImage(selectedImage)?.style.insetSize ?? 0) - 4\n                      ).toString()\n                    : '0',\n              })\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/image-options/roundness-option.tsx",
    "content": "'use client'\n\nimport { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\n\nexport default function RoundnessOption() {\n  const { images, updateImageStyle, getImage } = useImageOptions()\n  const { setShowControls } = useMoveable()\n  const { selectedImage } = useSelectedLayers()\n\n  const browserFrame = selectedImage ? getImage(selectedImage)?.frame : 'None'\n\n  return (\n    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>\n      <div className=\"mb-3 mt-6 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Roundness</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round(\n            Number(\n              selectedImage\n                ? getImage(selectedImage)?.style.imageRoundness\n                : 0.2\n            ) * 10\n          )} `}\n        </p>\n      </div>\n\n      <div className=\"flex gap-4 px-1 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0.7]}\n          max={browserFrame !== 'None' && browserFrame !== 'Arc' ? 1.6 : 5}\n          min={0}\n          step={0.05}\n          onValueChange={(value) => {\n            setShowControls(false)\n            selectedImage &&\n              updateImageStyle(selectedImage, { imageRoundness: value[0] })\n          }}\n          value={\n            images.length !== 0 && selectedImage\n              ? [+(getImage(selectedImage)?.style.imageRoundness ?? 1)]\n              : [1]\n          }\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            if (images.length === 0 || !selectedImage) return\n            if (Number(getImage(selectedImage)?.style.imageRoundness) >= 5)\n              return\n            setShowControls(false)\n            updateImageStyle(selectedImage, {\n              imageRoundness:\n                Number(getImage(selectedImage)?.style.imageRoundness ?? 0) +\n                0.1,\n            })\n          }}\n          onDecrement={() => {\n            if (images.length === 0 || !selectedImage) return\n            if (Number(getImage(selectedImage)?.style.imageRoundness) <= 0)\n              return\n            setShowControls(false)\n            updateImageStyle(selectedImage, {\n              imageRoundness:\n                Number(getImage(selectedImage)?.style.imageRoundness ?? 0) -\n                0.1,\n            })\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/image-options/scale-options.tsx",
    "content": "import { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\n\ntype SizeOptionProps = {\n  text?: string\n}\n\nexport default function SizeOption({ text = 'Scale' }: SizeOptionProps) {\n  const { images, setImages, scale, setScale } = useImageOptions()\n  const { selectedImage } = useSelectedLayers()\n  const { setShowControls } = useMoveable()\n\n  return (\n    <>\n      <div className=\"mb-3 mt-2 flex items-center px-1 md:md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">{text}</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {Math.round(scale * 100)}%\n        </p>\n      </div>\n\n      <div className=\"flex gap-4 px-1 text-[0.85rem] md:md:max-w-full\">\n        <Slider\n          defaultValue={[1]}\n          max={3}\n          min={0.25}\n          step={0.01}\n          onValueChange={(value: number[]) => {\n            setShowControls(false)\n            setScale(value[0])\n          }}\n          onValueCommit={() => setShowControls(true)}\n          value={[scale ?? 1]}\n          onIncrement={() => {\n            if (scale >= 3) return\n            setScale(scale + 0.05)\n          }}\n          onDecrement={() => {\n            if (scale <= 0.25) return\n            setScale(scale - 0.05)\n          }}\n        />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/image-options/shadow-settings.tsx",
    "content": "'use client'\n\nimport PopupColorPicker from '@/components/popup-color-picker'\nimport { Button } from '@/components/ui/button'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { shadows } from '@/utils/presets/shadows'\nimport { ChevronDown } from 'lucide-react'\nimport { Slider } from '@/components/ui/slider'\nimport { useMoveable } from '@/store/use-moveable'\n\nexport default function ShadowSettings() {\n  const { images, setImages, defaultStyle } = useImageOptions()\n  const { showControls } = useMoveable()\n  const { selectedImage } = useSelectedLayers()\n\n  const { backgroundType } = useBackgroundOptions()\n\n  const boxShadowStyle = {\n    boxShadow: selectedImage\n      ? images[selectedImage - 1]?.style.imageShadow\n      : '',\n  }\n\n  const boxShadowPreview = {\n    boxShadow: selectedImage\n      ? images[selectedImage - 1]?.style.shadowPreview\n      : '',\n  }\n\n  const backgroundStyle = {\n    backgroundImage: `var(--gradient-bg)`,\n    backgroundColor:\n      backgroundType === 'mesh' ? `var(--mesh-bg)` : 'var(--solid-bg)',\n  }\n\n  const handleShadowButtonClick = (shadow: {\n    shadow: string\n    fullName: string\n    preview: string\n  }) => {\n    selectedImage &&\n      setImages(\n        images.map((image, index) =>\n          index === selectedImage - 1\n            ? {\n                ...image,\n                style: {\n                  ...image.style,\n                  imageShadow: shadow.shadow,\n                  shadowName: shadow.fullName,\n                  shadowPreview: shadow.preview,\n                },\n              }\n            : image\n        )\n      )\n  }\n\n  const handleColorChange = (color: string) => {\n    selectedImage &&\n      setImages(\n        images.map((image, index) =>\n          index === selectedImage - 1\n            ? {\n                ...image,\n                style: {\n                  ...image.style,\n                  shadowColor: color,\n                  imageShadow:\n                    shadows.find(\n                      (shadow) =>\n                        shadow.fullName ===\n                        (images[selectedImage - 1]?.style.shadowName ?? '')\n                    )?.shadow ?? '',\n                },\n              }\n            : image\n        )\n      )\n  }\n\n  return (\n    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>\n      <Popover>\n        <PopoverTrigger className=\"relative mt-2 flex h-14 w-full items-center overflow-hidden rounded-lg border border-border/80 bg-[#898beb05]\">\n          <div\n            style={backgroundStyle}\n            className=\"flex-center h-full basis-[23%]\"\n          >\n            <div\n              className=\"flex-center h-1/2 w-1/2 rounded-md bg-white\"\n              style={boxShadowPreview}\n            ></div>\n          </div>\n          <div className=\"flex h-full w-full flex-1 items-center justify-between px-4\">\n            <div className=\"flex w-full flex-col items-start\">\n              <p className=\"text-[0.85rem] font-medium text-dark/70\">\n                {selectedImage\n                  ? images[selectedImage - 1]?.style.shadowName\n                  : 'None'}\n              </p>\n              <p className=\"text-[0.7rem] font-bold text-dark/50\">\n                {selectedImage\n                  ? images[selectedImage - 1]?.style.shadowColor.slice(0, 7)\n                  : '#000'}\n              </p>\n            </div>\n\n            <ChevronDown size={18} className=\"text-dark/80\" />\n          </div>\n        </PopoverTrigger>\n        <PopoverContent\n          align=\"start\"\n          className=\"grid w-[350px] grid-cols-3 gap-4 rounded-lg bg-formDark p-4\"\n        >\n          {/* Inside popup  */}\n          {shadows.map((shadow) => (\n            <Button\n              variant=\"secondary\"\n              key={shadow.name}\n              onClick={() => {\n                handleShadowButtonClick(shadow)\n              }}\n              className={`flex-center relative h-20 w-24 cursor-pointer rounded-md ${\n                shadow.shadow ===\n                  images[selectedImage! - 1]?.style.imageShadow &&\n                'outline-none ring-2 ring-ring ring-offset-2'\n              }`}\n              style={backgroundStyle}\n            >\n              <div\n                className=\"flex-center h-[75%] w-[95%] rounded-md bg-white text-xs text-[#333]\"\n                style={{ boxShadow: `${shadow.preview}` }}\n              >\n                {shadow.name}\n              </div>\n            </Button>\n          ))}\n        </PopoverContent>\n      </Popover>\n\n      <div className=\"mb-3 mt-8 flex items-center px-1\">\n        <h1 className=\"text-[0.85rem]\">Opacity</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {Math.round(\n            Number(\n              selectedImage\n                ? images[selectedImage - 1]?.style.shadowOpacity ?? 0.5\n                : 0.5\n            ) * 100\n          )}\n          %\n        </p>\n      </div>\n\n      <div className=\"flex gap-4 px-1 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0.5]}\n          min={0}\n          max={1}\n          step={0.01}\n          onValueChange={(value) => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          shadowOpacity: value[0],\n                        },\n                      }\n                    : image\n                )\n              )\n          }}\n          value={\n            images.length !== 0 && selectedImage\n              ? [+images[selectedImage - 1]?.style.shadowOpacity]\n              : [1]\n          }\n          onIncrement={() => {\n            if (images.length === 0 || !selectedImage) return\n            if (Number(images[selectedImage - 1]?.style.shadowOpacity) >= 1)\n              return\n            setImages(\n              images.map((image, index) =>\n                index === selectedImage - 1\n                  ? {\n                      ...image,\n                      style: {\n                        ...image.style,\n                        shadowOpacity: Number(image.style.shadowOpacity) + 0.01,\n                      },\n                    }\n                  : image\n              )\n            )\n          }}\n          onDecrement={() => {\n            if (images.length === 0 || !selectedImage) return\n            if (Number(images[selectedImage - 1]?.style.shadowOpacity) <= 0)\n              return\n            setImages(\n              images.map((image, index) =>\n                index === selectedImage - 1\n                  ? {\n                      ...image,\n                      style: {\n                        ...image.style,\n                        shadowOpacity: Number(image.style.shadowOpacity) - 0.01,\n                      },\n                    }\n                  : image\n              )\n            )\n          }}\n        />\n      </div>\n\n      <div className=\"mb-3 mt-8 flex items-center px-1\">\n        <h1 className=\"text-[0.85rem]\">Shadow color</h1>\n      </div>\n\n      <PopupColorPicker\n        shouldShowAlpha={false}\n        color={\n          selectedImage ? images[selectedImage - 1]?.style.shadowColor : '#000'\n        }\n        onChange={handleColorChange}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/main-image-area.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport { useOnClickOutside } from '@/hooks/use-on-click-outside'\nimport demoImage from '@/public/images/demo-tweet.png'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nimport { useColorExtractor } from '@/store/use-color-extractor'\nimport { useFrameOptions } from '@/store/use-frame-options'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport {\n  calculateEqualCanvasSize,\n  convertHexToRgba,\n  splitWidthHeight,\n} from '@/utils/helper-fns'\nimport { ImageIcon, Upload } from 'lucide-react'\nimport { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'\nimport Dropzone from 'react-dropzone'\nimport { Button } from '../ui/button'\nimport BrowserFrame from './browser-frames'\nimport ContextMenuImage from './image-context-menu'\n\nconst ImageUpload = () => {\n  const targetRef = useRef<HTMLDivElement>(null)\n  const {\n    images,\n    addImage,\n    updateImage,\n    updateImageStyle,\n\n    setInitialImageUploaded,\n    initialImageUploaded,\n  } = useImageOptions()\n  const { selectedImage, setSelectedImage, setSelectedText } = useSelectedLayers()\n  const { setShowControls, isSelecting, isMultipleTargetSelected } =\n    useMoveable()\n  const { exactDomResolution } = useResizeCanvas()\n  const { width: exactDomWidth, height: exactDomHeight } =\n    splitWidthHeight(exactDomResolution)\n  const { frameHeight, showStroke, arcDarkMode } = useFrameOptions()\n  const { imagesCheck } = useColorExtractor()\n\n  useEffect(() => {\n    if (images.length === 0) {\n      return\n    }\n    setInitialImageUploaded(true)\n\n    const extractColors = async () => {\n      const analyze = (await import('rgbaster')).default\n\n      const result = await analyze(images[images.length - 1].image, {\n        scale: 0.5,\n      })\n\n      const extractedColors = result.slice(0, 12)\n\n      updateImage(images.length, { extractedColors })\n      updateImageStyle(images.length, {\n        insetColor: extractedColors[0].color,\n      })\n    }\n    extractColors()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [imagesCheck])\n  console.log(images)\n\n  useOnClickOutside(targetRef, () => {\n    if (isMultipleTargetSelected) return\n    // setShowControls(false)\n  })\n\n  // useOnClickOutside(multipleTargetRef, () => {\n  //   setShowControls(false)\n\n  //   setIsSelecting(false)\n  // })\n\n  // const {\n  //   imageSize,\n  //   imageRoundness,\n  //   imageShadow,\n  //   borderSize,\n  //   borderColor,\n  //   rotate,\n  // } = images[selectedImage - 1]?.style || {}\n\n  return (\n    <>\n      {!initialImageUploaded && <LoadAImage />}\n      {images && (\n        <>\n          {images.map((image, index) => {\n            if (image.image !== '')\n              return (\n                <ContextMenuImage key={image.id + index}>\n                  <div\n                    className={`image image-check absolute z-[2] flex-1 overflow-hidden ${\n                      isSelecting ? 'selectable' : ''\n                    } ${selectedImage ? '' : ''}`}\n                    ref={\n                      !isMultipleTargetSelected\n                        ? image.id === selectedImage\n                          ? targetRef\n                          : null\n                        : targetRef\n                    }\n                    style={{\n                      // transition:\n                      //   'box-shadow 0.8s cubic-bezier(0.6, 0.6, 0, 1)',\n                      transformStyle: 'preserve-3d',\n                      transformOrigin: `50% 50%`,\n                      // rotate: `${image.style.rotate}deg`,\n                      // transform: `scale(${image.style.imageSize}) rotateX(${image.style.rotateX}deg) rotateY(${image.style.rotateY}deg) rotateZ(${image.style.rotateZ}deg) `,\n                      transform: `perspective(${image.style.perspective}px) translate(${image.style.translateX}%, ${image.style.translateY}%) scale(${image.style.imageSize}) rotate(${image.style.rotate}deg) rotateX(${image.style.rotateX}deg) rotateY(${image.style.rotateY}deg) rotateZ(${image.style.rotateZ}deg)`,\n                      borderRadius: `${image.style.imageRoundness}rem`,\n                      boxShadow:\n                        image.style.shadowName !== 'Medium'\n                          ? `${image.style.imageShadow} ${convertHexToRgba(\n                              image.style.shadowColor,\n                              image.style.shadowOpacity\n                            )}${\n                              image.frame === 'Shadow'\n                                ? ',11px 11px rgba(0,0,0,0.8)'\n                                : ''\n                            }`\n                          : `0px 18px 88px -4px ${convertHexToRgba(\n                              image.style.shadowColor,\n                              image.style.shadowOpacity\n                            )}, 0px 8px 28px -6px ${convertHexToRgba(\n                              image.style.shadowColor,\n                              image.style.shadowOpacity\n                            )}${\n                              image.frame === 'Shadow'\n                                ? ',11px 11px rgba(0,0,0,0.8)'\n                                : ''\n                            }`,\n\n                      padding:\n                        image.frame !== 'None'\n                          ? image.frame === 'Arc'\n                            ? frameHeight === 'small'\n                              ? '10px'\n                              : frameHeight === 'medium'\n                              ? '13px'\n                              : '15px'\n                            : ''\n                          : `${image.style.insetSize}px`,\n\n                      backgroundColor:\n                        image.style.insetSize !== '0' && image.frame === 'None'\n                          ? `${image?.style.insetColor}`\n                          : image.frame === 'Arc'\n                          ? arcDarkMode\n                            ? '#00000050'\n                            : '#ffffff50'\n                          : image.frame === 'Shadow'\n                          ? 'rgba(0,0,0,0.8)'\n                          : 'transparent',\n\n                      border:\n                        image.frame === 'Arc'\n                          ? arcDarkMode\n                            ? '1px solid #00000020'\n                            : '1px solid #ffffff60'\n                          : image.frame === 'Shadow'\n                          ? showStroke\n                            ? '3px solid rgba(0,0,0,0.8)'\n                            : ''\n                          : '',\n\n                      zIndex: `${image.style.zIndex}`,\n                    }}\n                    id={`${image.id}`}\n                    onClick={() => {\n                      setShowControls(true)\n                      setSelectedImage(image.id)\n                    }}\n                    // on right click too do the same\n                    onContextMenu={(e) => {\n                      setShowControls(true)\n                      setSelectedImage(image.id)\n                    }}\n                  >\n                    <BrowserFrame frame={image.frame || 'None'} />\n\n                    <img\n                      draggable={false}\n                      className={`pointer-events-none h-full w-full shrink-0 ${\n                        image.frame === 'Arc' ? 'shadow-md' : ''\n                      }`}\n                      id={`img-${image.id}`}\n                      src={image.image}\n                      alt=\"Uploaded image\"\n                      style={{\n                        borderRadius:\n                          image.frame !== 'None'\n                            ? image.frame === 'Arc'\n                              ? `calc(${image.style.imageRoundness}rem - 9px)`\n                              : ''\n                            : `calc(${image.style.imageRoundness}rem - ${image.style.insetSize}px)`,\n\n                        padding:\n                          image.frame === 'None'\n                            ? ''\n                            : `${image.style.insetSize}px`,\n\n                        backgroundColor:\n                          image.style.insetSize !== '0' &&\n                          image.frame !== 'None'\n                            ? `${image?.style.insetColor}`\n                            : '',\n                      }}\n                    />\n                  </div>\n\n                  {/* Trying layout feature! */}\n                  {/* <div\n                    className={`flex flex-col image image-check absolute flex-1 z-[2]   ${\n                      isSelecting ? 'selectable' : ''\n                    } ${selectedImage ? '' : ''}`}\n                    style={{\n                      // width: '65%',\n                      // maxHeight: '35%',\n                      width: `${+image.style.imageSize * +exactDomWidth}px`,\n                      maxHeight: `${+image.style.imageSize * +exactDomHeight}px`,\n                      // transform: `translate(35%,50%) rotateX(40deg) rotate(50deg) scale(1.3) skew(8deg, 0deg)`,\n                      borderRadius: `${image.style.imageRoundness}rem`,\n                      boxShadow:\n                        image.style.shadowName !== 'Medium'\n                          ? `${image.style.imageShadow} ${convertHexToRgba(\n                              image.style.shadowColor,\n                              image.style.shadowOpacity\n                            )}${\n                              image.frame === 'Shadow'\n                                ? ',11px 11px rgba(0,0,0,0.8)'\n                                : ''\n                            }`\n                          : `0px 18px 88px -4px ${convertHexToRgba(\n                              image.style.shadowColor,\n                              image.style.shadowOpacity\n                            )}, 0px 8px 28px -6px ${convertHexToRgba(\n                              image.style.shadowColor,\n                              image.style.shadowOpacity\n                            )}${\n                              image.frame === 'Shadow'\n                                ? ',11px 11px rgba(0,0,0,0.8)'\n                                : ''\n                            }`,\n                    }}\n                    ref={\n                      !isMultipleTargetSelected\n                        ? image.id === selectedImage\n                          ? targetRef\n                          : null\n                        : targetRef\n                    }\n                    id={`${image.id}`}\n                    onClick={() => {\n                      setShowControls(true)\n                      setSelectedImage(image.id)\n                    }}\n                    onContextMenu={(e) => {\n                      setShowControls(true)\n                      setSelectedImage(image.id)\n                    }}\n                  >\n                    <BrowserFrame frame={image.frame || 'None'} />\n                    <img\n                      draggable={false}\n                      className={`pointer-events-none h-full w-full shrink-0 object-cover object-center ${\n                        image.frame === 'Arc' ? 'shadow-md' : ''\n                      }`}\n                      id={`img-${image.id}`}\n                      src={image.image}\n                      alt=\"Uploaded image\"\n                      style={{\n                        borderRadius:\n                          image.frame !== 'None'\n                            ? image.frame === 'Arc'\n                              ? `calc(${image.style.imageRoundness}rem - 9px)`\n                              : ''\n                            : `calc(${image.style.imageRoundness}rem - ${image.style.insetSize}px)`,\n\n                        padding:\n                          image.frame === 'None'\n                            ? ''\n                            : `${image.style.insetSize}px`,\n\n                        backgroundColor:\n                          image.style.insetSize !== '0' &&\n                          image.frame !== 'None'\n                            ? `${image?.style.insetColor}`\n                            : '',\n                      }}\n                    />\n                  </div> */}\n                </ContextMenuImage>\n              )\n          })}\n        </>\n      )}\n    </>\n  )\n}\n\nexport default ImageUpload\n\nfunction LoadAImage() {\n  const {\n    images,\n    addImage,\n    updateImage,\n    defaultStyle,\n    setInitialImageUploaded,\n  } = useImageOptions()\n  const { setSelectedImage } = useSelectedLayers()\n  const { imagesCheck, setImagesCheck } = useColorExtractor()\n  const { setResolution, automaticResolution } = useResizeCanvas()\n  const { setBackground } = useBackgroundOptions()\n  const [isDragging, setIsDragging] = useState<boolean>(false)\n\n  useEffect(() => {\n    const handlePaste = async (event: ClipboardEvent) => {\n      const items = event.clipboardData?.items\n      if (!items) return\n\n      const itemsArray = Array.from(items)\n      for (const item of itemsArray) {\n        if (item.type.indexOf('image') === 0) {\n          const file = item.getAsFile()\n          if (file) {\n            const imageUrl = URL.createObjectURL(file)\n            setInitialImageUploaded(true)\n            setImagesCheck([...imagesCheck, imageUrl])\n            addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })\n            setSelectedImage(images.length + 1)\n\n            if (images.length === 0 && automaticResolution) {\n              const padding = 250\n              const img = new Image()\n              img.src = imageUrl\n\n              img.onload = () => {\n                const { naturalWidth, naturalHeight } = img\n                const newResolution = calculateEqualCanvasSize(\n                  naturalWidth,\n                  naturalHeight,\n                  padding\n                )\n                setResolution(newResolution.toString())\n              }\n            }\n          }\n        }\n      }\n    }\n\n    document.addEventListener('paste', handlePaste)\n    return () => document.removeEventListener('paste', handlePaste)\n  }, [images, imagesCheck, addImage, setImagesCheck, setInitialImageUploaded, setSelectedImage, defaultStyle, automaticResolution, setResolution])\n\n  const handleImageLoad = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      const file = event.target.files?.[0]\n\n      if (file) {\n        const imageUrl = URL.createObjectURL(file)\n        setInitialImageUploaded(true)\n\n        setImagesCheck([...imagesCheck, imageUrl])\n        addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })\n        setSelectedImage(images.length + 1)\n\n        if (images.length > 0) return\n        if (automaticResolution) {\n          const padding = 200\n          const img = new Image()\n          img.src = imageUrl\n\n          img.onload = () => {\n            const { naturalWidth, naturalHeight } = img\n            const newResolution = calculateEqualCanvasSize(\n              naturalWidth,\n              naturalHeight,\n              padding\n            )\n            setResolution(newResolution.toString())\n          }\n        }\n      }\n    },\n    [\n      setInitialImageUploaded,\n      setImagesCheck,\n      imagesCheck,\n      images,\n      defaultStyle,\n      setSelectedImage,\n      automaticResolution,\n      setResolution,\n    ]\n  )\n\n  const handleImageChange = useCallback(\n    (file: any) => {\n      // const file = event.target.files?.[0]\n\n      if (file) {\n        const imageUrl = URL.createObjectURL(file)\n        setInitialImageUploaded(true)\n\n        setImagesCheck([...imagesCheck, imageUrl])\n        addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })\n        setSelectedImage(images.length + 1)\n\n        if (images.length > 0) return\n        if (automaticResolution) {\n          const padding = 250\n          const img = new Image()\n          img.src = imageUrl\n\n          img.onload = () => {\n            const { naturalWidth, naturalHeight } = img\n            const newResolution = calculateEqualCanvasSize(\n              naturalWidth,\n              naturalHeight,\n              padding\n            )\n            setResolution(newResolution.toString())\n          }\n        }\n      }\n    },\n    [\n      setInitialImageUploaded,\n      setImagesCheck,\n      imagesCheck,\n      images,\n      defaultStyle,\n      setSelectedImage,\n      automaticResolution,\n      setResolution,\n    ]\n  )\n\n  const loadDemoImage = () => {\n    if (typeof window === 'undefined') return\n    setBackground('linear-gradient(var(--gradient-angle), #898aeb, #d8b9e3)')\n    document?.documentElement.style.setProperty(\n      '--gradient-bg',\n      ' linear-gradient(var(--gradient-angle), #898aeb, #d8b9e3)'\n    )\n    addImage({\n      image: demoImage.src,\n      id: 1,\n      style: {\n        ...defaultStyle,\n        borderSize: '15',\n        imageRoundness: 0.7,\n        imageSize: '0.78',\n        insetSize: '10',\n      },\n    })\n    setImagesCheck([...imagesCheck, demoImage.src])\n    setResolution('1920x1080')\n  }\n\n  return (\n    <Dropzone\n      multiple={false}\n      onDrop={(acceptedFiles) => {\n        handleImageChange(acceptedFiles[0])\n      }}\n      onDragEnter={() => setIsDragging(true)}\n      onDragLeave={() => setIsDragging(false)}\n      noClick\n    >\n      {({ getRootProps, getInputProps }) => (\n        <div\n          {...getRootProps()}\n          className=\"h-25 absolute-center w-4/5 xl:w-2/5\"\n        >\n          <div className=\"flex flex-col gap-4 rounded-xl  text-center  md:shadow-2xl\">\n            <div className=\"flex-center flex-col rounded-xl px-4 py-10 md:bg-[#f1f1f1]\">\n              <Upload\n                style={{\n                  transition: 'all 0.8s cubic-bezier(0.6, 0.6, 0, 1)',\n                }}\n                className={`mx-auto mb-2 hidden h-10 w-10 text-[#332]/80 sm:block ${\n                  isDragging ? 'rotate-180' : 'rotate-0'\n                }`}\n                aria-hidden=\"true\"\n              />\n              <div className=\"flex-center mt-4 text-base leading-6 text-gray-400\">\n                <label\n                  htmlFor=\"file-upload\"\n                  className=\"focus-within:ring-purple relative cursor-pointer rounded-md font-bold text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 hover:text-purple\"\n                >\n                  <span>Load a image</span>\n                </label>\n                <input {...getInputProps()} />\n                <input\n                  id=\"file-upload\"\n                  name=\"file-upload\"\n                  type=\"file\"\n                  onChange={handleImageLoad}\n                  accept=\"image/*\"\n                  className=\"sr-only\"\n                />\n                <p className=\"hidden pl-1 font-medium text-[#333]/80 lg:block\">\n                  or drag and drop\n                </p>\n              </div>\n\n              <p className=\"mt-4 hidden text-sm font-extrabold leading-5 text-[#555]/80 sm:block\">\n                OR\n              </p>\n\n              <Button\n                onClick={loadDemoImage}\n                className=\"z-[120] mt-4 hidden rounded-md border-[#898aeb]/40 bg-[#898aeb]/30 font-semibold text-[#6264aa] shadow-sm sm:inline-flex\"\n                variant=\"stylish\"\n              >\n                Try with a demo image\n                <ImageIcon className=\"ml-2\" size={19} />\n              </Button>\n            </div>\n          </div>\n        </div>\n      )}\n    </Dropzone>\n  )\n}\n"
  },
  {
    "path": "components/editor/mobile-view-image-options.tsx",
    "content": "import { ScrollArea } from '@/components/ui/scroll-area'\nimport BackgroundOptions from '@/components/editor/background-options'\nimport CanvasOptions from '@/components/editor/canvas-options'\nimport FrameOptions from '@/components/editor/frame-options'\nimport ImageOptions from '@/components/editor/image-options'\nimport PerspectiveOptions from '@/components/editor/perspective-options'\nimport PositionOptions from '@/components/editor/position-options'\nimport TextOptions from '@/components/editor/text-options'\nimport { useActiveIndexStore } from '@/store/use-active-index'\n\nexport default function MobileViewImageOptions() {\n  const { activeIndex } = useActiveIndexStore()\n\n  return (\n    <ScrollArea className=\"mt-6 w-full md:hidden\" type=\"auto\">\n      <div className=\"w-full max-w-[90%] md:hidden\">\n        {activeIndex === 0 && <CanvasOptions />}\n        {activeIndex === 1 && <ImageOptions />}\n        {activeIndex === 2 && <BackgroundOptions />}\n        {activeIndex === 3 && <FrameOptions />}\n        {activeIndex === 4 && <TextOptions />}\n        {activeIndex === 5 && <PerspectiveOptions />}\n        {activeIndex === 6 && <PositionOptions />}\n      </div>\n    </ScrollArea>\n  )\n}\n"
  },
  {
    "path": "components/editor/moveable-component.tsx",
    "content": "'use client'\n\nimport React from 'react'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\n\nimport { useImageQualityStore } from '@/store/use-image-quality'\nimport { useMoveable } from '@/store/use-moveable'\nimport { splitWidthHeight } from '@/utils/helper-fns'\nimport {\n  Draggable,\n  DraggableProps,\n  GroupableProps,\n  Rotatable,\n  RotatableProps,\n  Scalable,\n  ScalableProps,\n  Snappable,\n  SnappableProps,\n  makeMoveable,\n} from 'react-moveable'\n\nconst Moveable = makeMoveable<\n  DraggableProps &\n    ScalableProps &\n    RotatableProps &\n    SnappableProps &\n    GroupableProps\n  // @ts-ignore\n>([Draggable, Scalable, Rotatable, Snappable])\n\nexport default function MoveableComponent({ id }: { id: string }) {\n  const { quality } = useImageQualityStore()\n  const { domResolution, scaleFactor, exactDomResolution } = useResizeCanvas()\n  const { setImages, images } = useImageOptions()\n  const { selectedImage } = useSelectedLayers()\n  const moveableRef = React.useRef<any>(null)\n  const { width, height } = splitWidthHeight(exactDomResolution)\n  const { isMultipleTargetSelected } = useMoveable()\n\n  const otherImages = images.filter((image) => image.id !== selectedImage)\n  const elementGuidelines = otherImages.map((image) => ({\n    element:\n      typeof document !== 'undefined'\n        ? document?.getElementById(`${image.id}`)\n        : '',\n  }))\n\n  const [domWidth, domHeight]: number[] = domResolution.split('x').map(Number)\n  return (\n    <Moveable\n      ref={moveableRef}\n      target={\n        isMultipleTargetSelected\n          ? '.selected'\n          : typeof document !== 'undefined'\n          ? document?.getElementById(id)\n          : ''\n      }\n      hideChildMoveableDefaultLines={true}\n      draggable={true}\n      onDrag={({ target, beforeTranslate }) => {\n        const perspective = target.style.transform.match(/perspective\\((.*?)\\)/)\n        // @ts-expect-error\n        const xPerc = (beforeTranslate[0] / target?.offsetWidth) * 100\n        // @ts-expect-error\n        const yPerc = (beforeTranslate[1] / target?.offsetHeight) * 100\n\n        const scale = target.style.transform.match(/scale\\((.*?)\\)/)\n\n        const rotate = target.style.transform.match(/rotate\\((.*?)\\)/)\n\n        const rotateX = target.style.transform.match(/rotateX\\((.*?)\\)/)\n        const rotateY = target.style.transform.match(/rotateY\\((.*?)\\)/)\n        const rotateZ = target.style.transform.match(/rotateZ\\((.*?)\\)/)\n\n        target.style.transform = `${\n          perspective ? perspective[0] : ''\n        } translate(${xPerc}%, ${yPerc}%) ${scale ? scale[0] : ''} ${\n          rotate ? rotate[0] : ''\n        } ${rotateX ? rotateX[0] : ''} ${rotateY ? rotateY[0] : ''} ${\n          rotateZ ? rotateZ[0] : ''\n        } `\n      }}\n      onDragEnd={({ target, lastEvent }) => {\n        // @ts-expect-error\n        const xPerc = (lastEvent?.translate[0] / target?.offsetWidth) * 100\n        // @ts-expect-error\n        const yPerc = (lastEvent?.translate[1] / target?.offsetHeight) * 100\n\n        selectedImage &&\n          setImages(\n            images.map((image, index) =>\n              index === selectedImage - 1\n                ? {\n                    ...image,\n                    style: {\n                      ...image.style,\n                      translateX: xPerc,\n                      translateY: yPerc,\n                    },\n                  }\n                : image\n            )\n          )\n      }}\n      scalable={true}\n      keepRatio={true}\n      onScale={({ scale, target, drag }) => {\n        const perspective = target.style.transform.match(/perspective\\((.*?)\\)/)\n        const rotateX = target.style.transform.match(/rotateX\\((.*?)\\)/)\n\n        const scaleX = scale[0]\n        const scaleY = scale[1]\n\n        const rotate = target.style.transform.match(/rotate\\((.*?)\\)/)\n\n        const rotateY = target.style.transform.match(/rotateY\\((.*?)\\)/)\n        const rotateZ = target.style.transform.match(/rotateZ\\((.*?)\\)/)\n\n        // @ts-expect-error\n        const xPerc = (drag.beforeTranslate[0] / target.offsetWidth) * 100\n        // @ts-expect-error\n        const yPerc = (drag.beforeTranslate[1] / target.offsetHeight) * 100\n\n        target.style.transform = `${\n          perspective ? perspective[0] : ''\n        } translate(${xPerc}%, ${yPerc}%) scale(${scaleX}, ${scaleY}) ${\n          rotate ? rotate[0] : ''\n        } ${rotateX ? rotateX[0] : ''} ${rotateY ? rotateY[0] : ''} ${\n          rotateZ ? rotateZ[0] : ''\n        }`\n      }}\n      onScaleEnd={({ target, lastEvent }) => {\n        const scaleX = lastEvent.scale[0]\n        // @ts-expect-error\n        const xPerc = (lastEvent?.drag?.translate[0] / target.offsetWidth) * 100\n        const yPerc =\n          // @ts-expect-error\n          (lastEvent?.drag?.translate[1] / target.offsetHeight) * 100\n\n        selectedImage &&\n          setImages(\n            images.map((image, index) =>\n              index === selectedImage - 1\n                ? {\n                    ...image,\n                    style: {\n                      ...image.style,\n                      translateX: xPerc,\n                      translateY: yPerc,\n                      imageSize: `${scaleX}`,\n                    },\n                  }\n                : image\n            )\n          )\n      }}\n      rotatable={!isMultipleTargetSelected}\n      rotationPosition={'top'}\n      onRotate={({ target, beforeRotate }) => {\n        const scale = target.style.transform.match(/scale\\((.*?)\\)/)\n        const translate = target.style.transform.match(/translate\\((.*?)\\)/)\n\n        const perspective = target.style.transform.match(/perspective\\((.*?)\\)/)\n        const rotateX = target.style.transform.match(/rotateX\\((.*?)\\)/)\n        const rotateY = target.style.transform.match(/rotateY\\((.*?)\\)/)\n        const rotateZ = target.style.transform.match(/rotateZ\\((.*?)\\)/)\n\n        const rotate = beforeRotate || ''\n\n        target.style.transform = `${perspective ? perspective[0] : ''} ${\n          translate ? translate[0] : ''\n        } ${scale ? scale[0] : ''} ${rotate ? `rotate(${rotate}deg)` : ''} ${\n          rotateX ? rotateX[0] : ''\n        } ${rotateY ? rotateY[0] : ''} ${rotateZ ? rotateZ[0] : ''}`\n      }}\n      onRotateEnd={({ target, lastEvent }) => {\n        const rotate = lastEvent.rotate\n\n        selectedImage &&\n          setImages(\n            images.map((image, index) =>\n              index === selectedImage - 1\n                ? {\n                    ...image,\n                    style: {\n                      ...image.style,\n                      rotate: rotate,\n                    },\n                  }\n                : image\n            )\n          )\n      }}\n      snapRotationThreshold={2}\n      snapRotationDegrees={[0, 90, 180, 270]}\n      snappable={true}\n      snapDirections={{\n        center: true,\n        middle: true,\n        left: true,\n        top: true,\n        right: true,\n        bottom: true,\n      }}\n      snapThreshold={7}\n      horizontalGuidelines={[\n        domHeight / 2 / scaleFactor / quality,\n        domHeight / 1 / scaleFactor / quality,\n        0,\n      ]}\n      verticalGuidelines={[\n        domWidth / 2 / scaleFactor / quality,\n        domWidth / 1 / scaleFactor / quality,\n        0,\n      ]}\n      elementSnapDirections={{\n        top: true,\n        left: true,\n        bottom: true,\n        right: true,\n        center: true,\n        middle: true,\n      }}\n      elementGuidelines={!isMultipleTargetSelected ? elementGuidelines : []}\n      onRenderGroup={({ targets, events }) => {\n        if (!isMultipleTargetSelected) return\n\n        events.forEach((ev) => {\n          ev.target.style.transform = ev.transform\n        })\n      }}\n      onRenderGroupEnd={({ targets, events }) => {\n        if (isMultipleTargetSelected) {\n          const updatedImages = images.map((image, index) => {\n            const targetIndex = events.findIndex(\n              (ev: any) => ev.target.id === `${image.id}`\n            )\n            const updatedEvent = events[targetIndex]\n\n            if (targetIndex !== -1) {\n              const updatedEvent = events[targetIndex]\n\n              const xPerc =\n                (updatedEvent?.transformObject?.translate[0] /\n                  // @ts-expect-error\n                  targets[targetIndex]?.offsetWidth) *\n                100\n              const yPerc =\n                (updatedEvent?.transformObject?.translate[1] /\n                  // @ts-expect-error\n                  targets[targetIndex]?.offsetHeight) *\n                100\n\n              return {\n                ...image,\n                style: {\n                  ...image.style,\n                  translateX: xPerc,\n                  translateY: yPerc,\n                  imageSize: `${updatedEvent.transformObject.scale[0]}`,\n                },\n              }\n            }\n\n            // Return the original image if the target is not found\n            return image\n          })\n\n          setImages(updatedImages)\n        }\n      }}\n    />\n  )\n}\n"
  },
  {
    "path": "components/editor/noise.tsx",
    "content": "import { useBackgroundOptions } from '@/store/use-background-options'\n\nexport default function Noise() {\n  const { noise, isBackgroundClicked } = useBackgroundOptions()\n  return (\n    <>\n      {/* eslint-disable-next-line @next/next/no-img-element */}\n     {isBackgroundClicked && <img\n        draggable={false}\n        className={`pointer-events-none absolute z-[0] h-full w-full object-cover`}\n        style={{\n          opacity: noise,\n        }}\n        src={'/images/Noise.svg'}\n        alt=\"noise\"\n        loading=\"lazy\"\n      />}\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/perspective-options/index.tsx",
    "content": "import { JoystickIcon, Rotate3d } from 'lucide-react'\nimport RotateOptions from './rotate-options'\nimport { Joystick } from 'react-joystick-component'\nimport { IJoystickUpdateEvent } from 'react-joystick-component/build/lib/Joystick'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\n\nexport default function PerspectiveOptions() {\n  const { images, setImages } = useImageOptions()\n   const { selectedImage } = useSelectedLayers()\n  const { setShowControls } = useMoveable()\n\n  return (\n    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>\n      <h3 className=\"mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n        <Rotate3d size={20} />\n        <span>Custom options</span>\n      </h3>\n      <RotateOptions />\n      <hr className=\"my-8\" />\n      <h3 className=\"mb-6 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n        <JoystickIcon size={20} />\n        <span>Or with a controller</span>\n      </h3>\n      <Joystick\n        size={40}\n        stickColor=\"#898aeb\"\n        baseColor=\"#898aeb40\"\n        move={(event: IJoystickUpdateEvent) => {\n          const { type, x, y } = event\n          if (type === 'move') {\n            setShowControls(false)\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateX: y! * 20,\n                          rotateY: x! * 20,\n                        },\n                      }\n                    : image\n                )\n              )\n          } else if (type === 'stop') {\n            setShowControls(true)\n          }\n        }}\n      ></Joystick>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/perspective-options/rotate-options.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useMoveable } from '@/store/use-moveable'\nimport { RotateCcw } from 'lucide-react'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\n\nexport default function RotateOptions() {\n  const { images, setImages } = useImageOptions()\n  const { selectedImage } = useSelectedLayers()\n  const { setShowControls } = useMoveable()\n\n  return (\n    <>\n      {/* Perspective */}\n      <div\n        className={`mb-3 mt-8 flex items-center px-1 md:max-w-full ${\n          selectedImage ? '' : 'pointer-events-none opacity-40'\n        }`}\n      >\n        <h1 className=\"text-[0.85rem]\">3D Depth</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round(\n            selectedImage ? images[selectedImage - 1]?.style.perspective : 0\n          )}px`}\n        </p>\n        <Button\n          aria-label=\"reset rotate x\"\n          variant=\"secondary\"\n          size=\"sm\"\n          className=\"ml-auto translate-x-2\"\n          onClick={() =>\n            selectedImage &&\n            setImages(\n              images.map((image, index) =>\n                index === selectedImage - 1\n                  ? {\n                      ...image,\n                      style: {\n                        ...image.style,\n                        perspective: 2000,\n                      },\n                    }\n                  : image\n              )\n            )\n          }\n        >\n          <RotateCcw size={15} className=\"text-dark/80\" />\n        </Button>\n      </div>\n\n      <div className=\"mb-3 flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={6500}\n          min={0}\n          step={0.0001}\n          value={\n            selectedImage\n              ? [images[selectedImage - 1]?.style.perspective]\n              : [2000]\n          }\n          onValueChange={(value: number[]) => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          perspective: value[0],\n                        },\n                      }\n                    : image\n                )\n              )\n            setShowControls(false)\n          }}\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.perspective >= 6500) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          perspective: Number(image.style.perspective) + 500,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n          onDecrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.perspective <= 0) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          perspective: Number(image.style.perspective) - 500,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n        />\n      </div>\n\n      <hr className=\"my-6\" />\n\n      {/* RotateX */}\n      <div className=\"mb-3 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Rotate X</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round(\n            selectedImage ? images[selectedImage - 1]?.style.rotateX : 0\n          )}px`}\n        </p>\n        <Button\n          aria-label=\"reset rotate x\"\n          variant=\"secondary\"\n          size=\"sm\"\n          className=\"ml-auto translate-x-2\"\n          onClick={() => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateX: 0.0001,\n                        },\n                      }\n                    : image\n                )\n              )\n          }}\n        >\n          <RotateCcw size={15} className=\"text-dark/80\" />\n        </Button>\n      </div>\n\n      <div className=\"mb-3 flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={180}\n          min={-180}\n          step={0.0001}\n          value={\n            selectedImage ? [images[selectedImage - 1]?.style.rotateX] : [0]\n          }\n          onValueChange={(value: number[]) => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateX: value[0],\n                        },\n                      }\n                    : image\n                )\n              )\n            setShowControls(false)\n          }}\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.rotateX >= 180) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateX: Number(image.style.rotateX) + 1,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n          onDecrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.rotateX <= -180) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateX: Number(image.style.rotateX) - 1,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n        />\n      </div>\n\n      {/* RotateY */}\n      <div className=\"mb-3 mt-3 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Rotate Y</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round(\n            selectedImage ? images[selectedImage - 1]?.style.rotateY : 0\n          )}px`}\n        </p>\n        <Button\n          aria-label=\"reset rotate y\"\n          variant=\"secondary\"\n          size=\"sm\"\n          className=\"ml-auto translate-x-2\"\n          onClick={() => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateY: 0,\n                        },\n                      }\n                    : image\n                )\n              )\n          }}\n        >\n          <RotateCcw size={15} className=\"text-dark/80\" />\n        </Button>\n      </div>\n\n      <div className=\"flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={180}\n          min={-180}\n          step={0.0001}\n          value={\n            selectedImage ? [images[selectedImage - 1]?.style.rotateY] : [0]\n          }\n          onValueChange={(value: number[]) => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateY: value[0],\n                        },\n                      }\n                    : image\n                )\n              )\n            setShowControls(false)\n          }}\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.rotateY >= 180) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateY: Number(image.style.rotateY) + 1,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n          onDecrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.rotateY <= -180) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateY: Number(image.style.rotateY) - 1,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n        />\n      </div>\n\n      {/* RotateZ */}\n      <div className=\"mb-3 mt-3 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Rotate Z</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round(\n            selectedImage ? images[selectedImage - 1]?.style.rotateZ : 0\n          )}px`}\n        </p>\n        <Button\n          aria-label=\"reset size\"\n          variant=\"secondary\"\n          size=\"sm\"\n          className=\"ml-auto translate-x-2\"\n          onClick={() =>\n            selectedImage &&\n            setImages(\n              images.map((image, index) =>\n                index === selectedImage - 1\n                  ? {\n                      ...image,\n                      style: {\n                        ...image.style,\n                        rotateZ: 0,\n                      },\n                    }\n                  : image\n              )\n            )\n          }\n        >\n          <RotateCcw size={15} className=\"text-dark/80\" />\n        </Button>\n      </div>\n\n      <div className=\"flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={180}\n          min={-180}\n          step={0.0001}\n          value={\n            selectedImage ? [images[selectedImage - 1]?.style.rotateZ] : [0]\n          }\n          onValueChange={(value: number[]) => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateZ: value[0],\n                        },\n                      }\n                    : image\n                )\n              )\n            setShowControls(false)\n          }}\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.rotateZ >= 180) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateZ: Number(image.style.rotateZ) + 1,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n          onDecrement={() => {\n            if (selectedImage) {\n              if (images[selectedImage - 1]?.style.rotateZ <= -180) return\n\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          rotateZ: Number(image.style.rotateZ) - 1,\n                        },\n                      }\n                    : image\n                )\n              )\n            }\n          }}\n        />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/position-options/index.tsx",
    "content": "'use client'\n\nimport SizeOption from '../image-options/scale-options'\nimport PositionControl from './position-control'\nimport TranslateOption from './translate-control'\n\nexport default function PositionOptions() {\n  return (\n    <>\n      <div className=\"mb-8\">\n        <div className=\"mb-4 mt-4 flex items-center px-1 md:max-w-full\">\n          <h1 className=\"text-[0.85rem]\">Position:</h1>\n        </div>\n\n        <PositionControl />\n      </div>\n\n      <div className=\"mb-2\">\n        <SizeOption text=\"Scale\" />\n      </div>\n\n      <div className=\"mb-4\">\n        <TranslateOption />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/position-options/position-control.tsx",
    "content": "'use client'\n\nimport {\n  CircleDot,\n  ArrowDown,\n  ArrowDownLeft,\n  ArrowDownRight,\n  ArrowLeft,\n  ArrowRight,\n  ArrowUp,\n  ArrowUpLeft,\n  ArrowUpRight,\n} from 'lucide-react'\nimport { useEffect, useCallback } from 'react'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\n\nexport default function PositionControl() {\n  const { images, setImages } = useImageOptions()\n  const { selectedImage } = useSelectedLayers()\n\n  const move = useCallback(\n    (deltaX: number, deltaY: number) => {\n      selectedImage &&\n        setImages(\n          images.map((image, index) =>\n            index === selectedImage - 1\n              ? {\n                  ...image,\n                  style: {\n                    ...image.style,\n                    translateX:\n                      images[selectedImage - 1]?.style.translateX + deltaX,\n                    translateY:\n                      images[selectedImage - 1]?.style.translateY + deltaY,\n                  },\n                }\n              : image\n          )\n        )\n    },\n    [images, setImages, selectedImage]\n  )\n\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      switch (event.key) {\n        case 'ArrowUp':\n          move(0, -5)\n          break\n        case 'ArrowDown':\n          move(0, 5)\n          break\n        case 'ArrowLeft':\n          move(-5, 0)\n          break\n        case 'ArrowRight':\n          move(5, 0)\n          break\n        default:\n          break\n      }\n    }\n\n    window.addEventListener('keydown', handleKeyDown)\n    return () => {\n      window.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [move])\n\n  const centerImage = () => {\n    selectedImage &&\n      setImages(\n        images.map((image, index) =>\n          index === selectedImage - 1\n            ? {\n                ...image,\n                style: {\n                  ...image.style,\n                  translateX: 0,\n                  translateY: 0,\n                },\n              }\n            : image\n        )\n      )\n  }\n\n  return (\n    <div\n      className={`relative grid h-40 border-spacing-8 grid-cols-3 overflow-hidden rounded-lg border border-border bg-formDark p-1 md:max-w-full [&>*]:cursor-pointer [&>*]:border-dashed [&>*]:border-border [&>*]:transition-colors ${\n        selectedImage ? '' : 'pointer-events-none opacity-40'\n      }`}\n    >\n      <button\n        className=\"flex-center :hover:bg-dark border-b-[2px] border-r-[2px]\"\n        aria-label=\"Translate up left\"\n        onClick={() => move(-5, -5)}\n      >\n        <ArrowUpLeft size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center border-b-[2px] border-r-[2px] hover:bg-dark\"\n        aria-label=\"Translate up\"\n        onClick={() => move(0, -5)}\n      >\n        <ArrowUp size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center border-b-[2px] hover:bg-dark\"\n        aria-label=\"Translate up right\"\n        onClick={() => move(5, -5)}\n      >\n        <ArrowUpRight size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center border-b-[2px] border-r-[2px] hover:bg-dark\"\n        aria-label=\"Translate left\"\n        onClick={() => move(-5, 0)}\n      >\n        <ArrowLeft size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center border-b-[2px] border-r-[2px] hover:bg-dark\"\n        aria-label=\"Center image\"\n        onClick={centerImage}\n      >\n        <CircleDot size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center border-b-[2px] hover:bg-dark\"\n        aria-label=\"Translate right\"\n        onClick={() => move(5, 0)}\n      >\n        <ArrowRight size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center border-r-[2px] hover:bg-dark\"\n        aria-label=\"Translate down left\"\n        onClick={() => move(-5, 5)}\n      >\n        <ArrowDownLeft size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center border-r-[2px] hover:bg-dark\"\n        aria-label=\"Translate down\"\n        onClick={() => move(0, 5)}\n      >\n        <ArrowDown size={21} aria-hidden />\n      </button>\n      <button\n        className=\"flex-center hover:bg-dark\"\n        aria-label=\"Translate down right\"\n        onClick={() => move(5, 5)}\n      >\n        <ArrowDownRight size={21} aria-hidden />\n      </button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/position-options/translate-control.tsx",
    "content": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useMoveable } from '@/store/use-moveable'\nimport { RotateCcw } from 'lucide-react'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\n\nexport default function TranslateOption() {\n  const { images, setImages } = useImageOptions()\n  const { selectedImage } = useSelectedLayers()\n  const { setShowControls } = useMoveable()\n\n  return (\n    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>\n      <div className=\"mb-3 mt-2 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Translate X</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round(\n            selectedImage ? images[selectedImage - 1]?.style.translateX : 0\n          )}px`}\n        </p>\n        <Button\n          aria-label=\"reset size\"\n          variant=\"secondary\"\n          size=\"sm\"\n          className=\"ml-auto translate-x-2\"\n          onClick={() => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          translateX: 0,\n                        },\n                      }\n                    : image\n                )\n              )\n          }}\n        >\n          <RotateCcw size={15} className=\"text-dark/80\" />\n        </Button>\n      </div>\n\n      <div className=\"mb-3 flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={1000}\n          min={-1000}\n          step={0.001}\n          value={\n            selectedImage ? [images[selectedImage - 1]?.style.translateX] : [0]\n          }\n          onValueChange={(value: number[]) => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          translateX: value[0],\n                        },\n                      }\n                    : image\n                )\n              )\n            setShowControls(false)\n          }}\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            selectedImage &&\n              (images[selectedImage - 1]?.style.translateX >= 1000 ||\n                setImages(\n                  images.map((image, index) =>\n                    index === selectedImage - 1\n                      ? {\n                          ...image,\n                          style: {\n                            ...image.style,\n                            translateX: image.style.translateX + 1,\n                          },\n                        }\n                      : image\n                  )\n                ))\n          }}\n          onDecrement={() => {\n            selectedImage &&\n              (images[selectedImage - 1]?.style.translateX <= -1000 ||\n                setImages(\n                  images.map((image, index) =>\n                    index === selectedImage - 1\n                      ? {\n                          ...image,\n                          style: {\n                            ...image.style,\n                            translateX: image.style.translateX - 1,\n                          },\n                        }\n                      : image\n                  )\n                ))\n          }}\n        />\n      </div>\n\n      <div className=\"mb-3 mt-3 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Translate Y</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {`${Math.round(\n            selectedImage ? images[selectedImage - 1]?.style.translateY : 0\n          )}px`}\n        </p>\n        <Button\n          aria-label=\"reset size\"\n          variant=\"secondary\"\n          size=\"sm\"\n          className=\"ml-auto translate-x-2\"\n          onClick={() => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          translateY: 0,\n                        },\n                      }\n                    : image\n                )\n              )\n          }}\n        >\n          <RotateCcw size={15} className=\"text-dark/80\" />\n        </Button>\n      </div>\n\n      <div className=\"flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0]}\n          max={500}\n          min={-500}\n          step={0.001}\n          value={\n            selectedImage ? [images[selectedImage - 1]?.style.translateY] : [0]\n          }\n          onValueChange={(value: number[]) => {\n            selectedImage &&\n              setImages(\n                images.map((image, index) =>\n                  index === selectedImage - 1\n                    ? {\n                        ...image,\n                        style: {\n                          ...image.style,\n                          translateY: value[0],\n                        },\n                      }\n                    : image\n                )\n              )\n            setShowControls(false)\n          }}\n          onValueCommit={() => setShowControls(true)}\n          onIncrement={() => {\n            selectedImage &&\n              (images[selectedImage - 1]?.style.translateY >= 500 ||\n                setImages(\n                  images.map((image, index) =>\n                    index === selectedImage - 1\n                      ? {\n                          ...image,\n                          style: {\n                            ...image.style,\n                            translateY: image.style.translateY + 1,\n                          },\n                        }\n                      : image\n                  )\n                ))\n          }}\n          onDecrement={() => {\n            selectedImage &&\n              (images[selectedImage - 1]?.style.translateY <= -500 ||\n                setImages(\n                  images.map((image, index) =>\n                    index === selectedImage - 1\n                      ? {\n                          ...image,\n                          style: {\n                            ...image.style,\n                            translateY: image.style.translateY - 1,\n                          },\n                        }\n                      : image\n                  )\n                ))\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/selecto-component.tsx",
    "content": "import React from 'react'\nimport { useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport dynamic from 'next/dynamic'\n\nconst Selecto = dynamic(\n  () => import('react-selecto').then((mod) => mod.default),\n  {\n    ssr: false,\n  }\n)\n\nexport default function SelectoComponent() {\n  const {\n    setShowControls,\n    showControls,\n    setIsSelecting,\n    setIsMultipleTargetSelected,\n  } = useMoveable()\n  const { setSelectedImage } = useSelectedLayers()\n\n  if (showControls) return\n  return (\n    <Selecto\n      dragContainer={'.canvas-container'}\n      selectableTargets={['.image']}\n      selectByClick={false}\n      selectFromInside={false}\n      toggleContinueSelect={['shift']}\n      ratio={0}\n      hitRate={0}\n      onSelectStart={(e) => {\n        setIsSelecting(true)\n      }}\n      onSelectEnd={(e) => {\n        setTimeout(() => {\n          setIsSelecting(false)\n        }, 100)\n        e.added.forEach((el) => {\n          el.classList.add('selected')\n        })\n\n        e.removed.forEach((el) => {\n          el.classList.remove('selected')\n        })\n\n        if (e?.selected.length !== 0) {\n          setShowControls(true)\n          setSelectedImage(+e?.selected?.[0]?.id)\n        }\n\n        if (e?.selected.length > 1) {\n          setIsMultipleTargetSelected(true)\n        }\n      }}\n    ></Selecto>\n  )\n}\n"
  },
  {
    "path": "components/editor/sidebar-buttons.tsx",
    "content": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { useActiveIndexStore } from '@/store/use-active-index'\n\nexport default function SidebarButton({\n  icon,\n  text,\n  index,\n}: {\n  text?: string\n  icon: React.ReactNode\n  index: number\n}) {\n  const activeIndex = useActiveIndexStore((state) => state.activeIndex)\n  const setActiveIndex = useActiveIndexStore((state) => state.setActiveIndex)\n\n  return (\n    <li\n      onClick={() => setActiveIndex(index)}\n      className={`click-ignored relative flex flex-col items-center gap-2`}\n    >\n      <Button\n        className={`h-11 rounded-xl px-3 py-2 md:h-12 md:px-4 md:py-3`}\n        variant={activeIndex === index ? 'stylish' : 'icon'}\n        aria-label={`${text} options`}\n      >\n        {icon}\n      </Button>\n      {text && (\n        <span\n          className={`hidden max-w-[3.25rem] truncate text-xs md:inline ${\n            activeIndex === index ? 'text-purple' : 'text-[#ababb1]'\n          }`}\n        >\n          {text}\n        </span>\n      )}\n    </li>\n  )\n}\n"
  },
  {
    "path": "components/editor/sidebar.tsx",
    "content": "'use client'\n\nimport BackgroundOptions from '@/components/editor/background-options'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { useActiveIndexStore } from '@/store/use-active-index'\nimport {\n  AppWindow,\n  Box,\n  Image as Images,\n  Locate,\n  Palette,\n  PanelTop,\n  TextCursor,\n} from 'lucide-react'\nimport React, { useMemo } from 'react'\nimport CanvasOptions from './canvas-options'\nimport FrameOptions from './frame-options'\nimport ImageOptions from './image-options'\nimport PerspectiveOptions from './perspective-options'\nimport PositionOptions from './position-options'\nimport SidebarButton from './sidebar-buttons'\nimport TextOptions from './text-options'\nimport { UndoRedoButtons, useUndoRedoHotkeys } from './undo-redo-buttons'\n\ntype SidebarSection =\n  | 'canvas'\n  | 'image'\n  | 'background'\n  | 'frame'\n  | 'text'\n  | '3d'\n  | 'position'\n\ninterface SidebarButton {\n  id: SidebarSection\n  text: string\n  icon: React.ElementType\n  component: React.ComponentType\n}\n\nfunction useSidebarButtons() {\n  return useMemo<SidebarButton[]>(\n    () => [\n      {\n        id: 'canvas',\n        text: 'Canvas',\n        icon: AppWindow,\n        component: CanvasOptions,\n      },\n      { id: 'image', text: 'Image', icon: Images, component: ImageOptions },\n      {\n        id: 'background',\n        text: 'Background',\n        icon: Palette,\n        component: BackgroundOptions,\n      },\n      { id: 'frame', text: 'Frame', icon: PanelTop, component: FrameOptions },\n      { id: 'text', text: 'Text', icon: TextCursor, component: TextOptions },\n      { id: '3d', text: '3D', icon: Box, component: PerspectiveOptions },\n      {\n        id: 'position',\n        text: 'Position',\n        icon: Locate,\n        component: PositionOptions,\n      },\n    ],\n    []\n  )\n}\n\nexport default function Sidebar() {\n  const sidebarButtons = useSidebarButtons()\n  const { activeIndex } = useActiveIndexStore()\n  const activeSection = sidebarButtons[activeIndex]?.id ?? 'image'\n  useUndoRedoHotkeys()\n\n  return (\n    <aside className=\"flex w-[6rem] overflow-x-hidden border-r border-border/60 md:min-w-[25rem] md:max-w-[25rem]\">\n      <ul className=\"no-scrollbar relative flex basis-[100%] flex-col items-center gap-6 overflow-x-hidden border-[#22262b]/60 bg-[#131313] px-4 py-8 md:max-w-[28%] md:basis-[28%] md:border-r\">\n        {sidebarButtons.map((button, index) => (\n          <SidebarButton\n            key={button.id}\n            text={button.text}\n            icon={\n              <button.icon\n                size={20}\n                strokeWidth={button.id === activeSection ? 2.25 : 2}\n              />\n            }\n            index={index}\n          />\n        ))}\n      </ul>\n      <div className=\"relative hidden h-full w-full flex-col overflow-hidden bg-[#151515] md:flex\">\n        <SidebarImageSettings sidebarButtons={sidebarButtons} />\n      </div>\n    </aside>\n  )\n}\n\ninterface SidebarImageSettingsProps {\n  sidebarButtons: SidebarButton[]\n}\n\nexport function SidebarImageSettings({\n  sidebarButtons,\n}: SidebarImageSettingsProps) {\n  const { activeIndex } = useActiveIndexStore()\n  const activeButton = sidebarButtons[activeIndex] ?? sidebarButtons[1] // Default to image if no active index\n  const ActiveComponent = activeButton?.component\n\n  return (\n    <ScrollArea type=\"scroll\">\n      <div className=\"flex flex-col px-[1.6rem]\">\n        <div className=\"flex w-full flex-col py-10\">\n          <h3 className=\"mb-8 flex items-center gap-2 text-xs font-semibold uppercase text-dark/70\">\n            {React.createElement(activeButton.icon, { size: 20 })}\n            <div>{activeButton.text}</div>\n            <UndoRedoButtons />\n          </h3>\n          {ActiveComponent && <ActiveComponent />}\n        </div>\n      </div>\n    </ScrollArea>\n  )\n}\n"
  },
  {
    "path": "components/editor/text-context-menu.tsx",
    "content": "import {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuTrigger,\n} from '@/components/ui/context-menu'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport { BringToFront, SendToBack, Trash } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nexport default function ContextMenuText({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  const { setTexts, texts } = useImageOptions()\n  const { selectedText, setSelectedText } = useSelectedLayers()\n  const { showTextControls, setShowTextControls } = useMoveable()\n\n  const handleTextDelete = (id: number) => {\n    if (texts.length === 1) {\n      setTexts([])\n      return\n    }\n    selectedText &&\n    setTexts(\n      texts.map((text, index) =>\n        index === selectedText - 1\n          ? {\n              ...text,\n              content: '',\n              text: '',\n            }\n          : text\n      )\n    )\n  }\n\n  const bringToFrontOrBack = (direction: 'front' | 'back') => {\n    if (selectedText) {\n      setTexts(\n        texts.map((text, index) =>\n          index === selectedText - 1\n            ? {\n                ...text,\n                style: {\n                  ...text.style,\n                  zIndex:\n                    direction === 'front'\n                      ? text.style.zIndex + 1\n                      : text.style.zIndex - 1,\n                },\n              }\n            : text\n        )\n      )\n    }\n  }\n\n  useHotkeys('Delete', () => {\n    if (showTextControls && selectedText) {\n      handleTextDelete(selectedText)\n      setShowTextControls(false)\n    }\n  })\n\n  return (\n    <ContextMenu>\n      <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>\n      <ContextMenuContent className=\"w-64\">\n        <ContextMenuItem\n          inset\n          onClick={() => {\n            bringToFrontOrBack('back')\n          }}\n          // disabled={!selectedText || texts[selectedText - 1].style.zIndex === 2}\n        >\n          Send back\n          <ContextMenuShortcut>\n            <BringToFront size={19} className=\"opacity-80\" />\n          </ContextMenuShortcut>\n        </ContextMenuItem>\n        <ContextMenuItem\n          inset\n          onClick={() => {\n            bringToFrontOrBack('front')\n          }}\n          // disabled={\n          // TODO:\n          //   // !selectedText ||\n          //   // texts[selectedText - 1].style.zIndex === texts.length\n          //   // do disbaled by adding length of both images and texts and check\n          // }\n        >\n          Bring forward\n          <ContextMenuShortcut>\n            <SendToBack size={19} className=\"opacity-80\" />\n          </ContextMenuShortcut>\n        </ContextMenuItem>\n\n        <ContextMenuSeparator />\n        <ContextMenuItem\n          inset\n          onClick={() => {\n            if (selectedText) {\n              handleTextDelete(selectedText)\n              setShowTextControls(false)\n            }\n          }}\n          className=\"text-[#F46567]/70 focus:text-[#f46567]/80\"\n        >\n          Delete\n          <ContextMenuShortcut>\n            <Trash size={19} className=\"text-[#F46567]/70 opacity-80\" />\n          </ContextMenuShortcut>\n        </ContextMenuItem>\n      </ContextMenuContent>\n    </ContextMenu>\n  )\n}\n"
  },
  {
    "path": "components/editor/text-layers.tsx",
    "content": "'use client'\n\nimport useTiptapEditor from '@/hooks/use-editor'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport { convertHexToRgba } from '@/utils/helper-fns'\nimport { BubbleMenu, Editor, EditorContent } from '@tiptap/react'\nimport { useRef } from 'react'\nimport ContextMenuText from './text-context-menu'\n\ntype MenuBarProps = {\n  editor: Editor | null\n}\n\nconst BubbleMenuComp = ({ editor }: MenuBarProps) => {\n  if (!editor) {\n    return null\n  }\n\n  return (\n    <>\n      <BubbleMenu\n        className=\"bubble-menu\"\n        tippyOptions={{\n          duration: 100,\n          followCursor: true,\n          placement: 'auto-end',\n        }}\n        editor={editor}\n      >\n        <button\n          onClick={() => {\n            const { selection } = editor.state\n\n            if (selection.empty) {\n              // When there's no text selection, apply formatting to the entire paragraph\n              editor.chain().focus().selectAll().toggleBold().run()\n            } else {\n              // When text is selected, toggle formatting for the selected text\n              editor.chain().focus().toggleBold().run()\n            }\n          }}\n          className={editor.isActive('bold') ? 'is-active' : ''}\n        >\n          bold\n        </button>\n        <button\n          onClick={() => {\n            const { selection } = editor.state\n\n            if (selection.empty) {\n              // When there's no text selection, apply formatting to the entire paragraph\n              editor.chain().focus().selectAll().toggleItalic().run()\n            } else {\n              // When text is selected, toggle formatting for the selected text\n              editor.chain().focus().toggleItalic().run()\n            }\n          }}\n          className={editor.isActive('italic') ? 'is-active' : ''}\n        >\n          italic\n        </button>\n\n        <input\n          type=\"color\"\n          onInput={(event: any) => {\n            editor.chain().focus().setColor(event.target.value).run()\n          }}\n          value={editor.getAttributes('textStyle').color}\n          data-testid=\"setColor\"\n        />\n      </BubbleMenu>\n    </>\n  )\n}\n\nfunction TipTapEditor() {\n  const { setShowTextControls, isEditable, setIsEditable } = useMoveable()\n  const { defaultStyle } = useImageOptions()\n\n  const { editor } = useTiptapEditor()\n\n  return (\n    <div\n      onDoubleClick={() => {\n        editor?.chain().selectAll().focus()\n        setIsEditable(true)\n        setShowTextControls(false)\n      }}\n    >\n      <BubbleMenuComp editor={editor} />\n      <div\n        className={`${\n          isEditable ? 'pointer-events-auto cursor-text' : 'pointer-events-none'\n        }`}\n      >\n        <EditorContent style={defaultStyle} editor={editor} />\n      </div>\n    </div>\n  )\n}\n\nexport default function TextLayers() {\n  const textRef = useRef<HTMLDivElement>(null)\n\n  const { setShowTextControls, setShowControls } = useMoveable()\n  const { texts } = useImageOptions()\n  const { selectedText, setSelectedText, setSelectedImage } =\n    useSelectedLayers()\n\n  return (\n    <>\n      {texts.map((text, index) => {\n        return (\n          <ContextMenuText key={text.id + index}>\n            <div\n              key={`text-${text.id}`}\n              id={`text-${text.id}`}\n              ref={text.id === selectedText ? textRef : null}\n              className={`text apply-font absolute flex-1 cursor-pointer  ${\n                text.content === '' ? 'pointer-events-none hidden' : 'image'\n              }`}\n              style={{\n                fontSize: `${text.style.textSize}rem`,\n                fontFamily: `${text.style.fontFamily}`,\n                color: `${text.style.textColor}`,\n                fontWeight: `${text.style.fontWeight}`,\n                textAlign: `${text.style.textAlign}`,\n                letterSpacing: `${text.style.letterSpacing}em`,\n                filter: `drop-shadow(${\n                  text.style.textShadow\n                } ${convertHexToRgba(\n                  text.style.shadowColor,\n                  text.style.shadowOpacity\n                )})`,\n                lineHeight: '1',\n                zIndex: `${text.style.zIndex}`,\n              }}\n              onContextMenu={() => {\n                setShowTextControls(true)\n                setSelectedText(text.id)\n                setSelectedImage(null)\n                setShowControls(false)\n              }}\n              onClick={() => {\n                setShowTextControls(true)\n                setSelectedText(text.id)\n                setSelectedImage(null)\n                setShowControls(false)\n              }}\n            >\n              <TipTapEditor />\n            </div>\n          </ContextMenuText>\n        )\n      })}\n    </>\n  )\n}\n"
  },
  {
    "path": "components/editor/text-options/add-text-layer.tsx",
    "content": "'use client'\n\nimport { Plus } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { useImageOptions } from '@/store/use-image-options'\n\nexport default function AddTextLayer() {\n  const { setTexts, defaultTextStyle, texts } = useImageOptions()\n\n  return (\n    <Button\n      onClick={() => {\n        setTexts([\n          ...texts,\n          {\n            content: 'Edit this text',\n            id: texts.length + 1,\n            style: defaultTextStyle,\n          },\n        ])\n      }}\n      size=\"lg\"\n      variant=\"stylish\"\n      className=\"w-full rounded-lg text-center text-base\"\n    >\n      <Plus size={22} className=\"mr-2 inline-block align-middle\" />\n      <span>Add a text layer</span>\n    </Button>\n  )\n}\n"
  },
  {
    "path": "components/editor/text-options/font-settings.tsx",
    "content": "'use client'\n\nimport PopupColorPicker from '@/components/popup-color-picker'\nimport { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport {\n  AlignHorizontalJustifyCenter,\n  AlignLeft,\n  AlignRight,\n  GalleryVerticalEnd,\n  Minus,\n  Plus,\n} from 'lucide-react'\nimport dynamic from 'next/dynamic'\n\nconst FontPicker = dynamic(\n  () => import('font-picker-react').then((mod) => mod.default),\n  {\n    ssr: false,\n  }\n)\n\nexport default function FontSettings() {\n  const { setTexts, texts } = useImageOptions()\n  const { selectedText } = useSelectedLayers()\n  const { setShowTextControls } = useMoveable()\n\n  const handleColorChange = (color: string) => {\n    selectedText &&\n      setTexts(\n        texts.map((text, index) =>\n          index === selectedText - 1\n            ? {\n                ...text,\n                style: {\n                  ...text.style,\n                  textColor: color,\n                },\n              }\n            : text\n        )\n      )\n  }\n\n  const handleShadowColorChange = (color: string) => {\n    selectedText &&\n      setTexts(\n        texts.map((text, index) =>\n          index === selectedText - 1\n            ? {\n                ...text,\n                style: {\n                  ...text.style,\n                  shadowColor: color,\n                },\n              }\n            : text\n        )\n      )\n  }\n\n  const handleAlignmentChange = (alignment: 'left' | 'center' | 'right') => {\n    selectedText &&\n      setTexts(\n        texts.map((text, index) =>\n          index === selectedText - 1\n            ? {\n                ...text,\n                style: {\n                  ...text.style,\n                  textAlign: alignment,\n                },\n              }\n            : text\n        )\n      )\n  }\n\n  const handleFontWeight = (weight: number) => {\n    selectedText &&\n      setTexts(\n        texts.map((text, index) =>\n          index === selectedText - 1\n            ? {\n                ...text,\n                style: {\n                  ...text.style,\n                  fontWeight: weight,\n                },\n              }\n            : text\n        )\n      )\n  }\n\n  return (\n    <div className=\"relative h-full w-full\">\n      <div className=\"mb-3 mt-2 flex items-center px-1 md:max-w-full\">\n        <h1 className=\"text-[0.85rem]\">Font family</h1>\n      </div>\n      {/* @ts-expect-error */}\n      <FontPicker\n        apiKey={process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY!}\n        activeFontFamily={\n          selectedText ? texts[selectedText - 1]?.style.fontFamily : 'Inter'\n        }\n        variants={['200', '300', 'regular', '500', '600', '700', '800']}\n        onChange={(font) => {\n          selectedText &&\n            setTexts(\n              texts.map((text, index) =>\n                index === selectedText - 1\n                  ? {\n                      ...text,\n                      style: {\n                        ...text.style,\n                        fontFamily: font.family,\n                      },\n                    }\n                  : text\n              )\n            )\n        }}\n      />\n\n      <div className={`mt-8 flex flex-col gap-3 px-1`}>\n        <h1 className=\"text-[0.85rem]\">Weight</h1>\n        <span className=\"inline-flex rounded-md shadow-sm\">\n          <button\n            type=\"button\"\n            className=\"relative -ml-px inline-flex items-center rounded-l-md bg-formDark px-2 py-2 text-dark ring-1 ring-inset ring-border focus:z-10 disabled:cursor-not-allowed\"\n            onClick={\n              selectedText && texts[selectedText - 1]?.style.fontWeight > 200\n                ? () =>\n                    handleFontWeight(\n                      texts[selectedText - 1]?.style.fontWeight - 100\n                    )\n                : () => handleFontWeight(200)\n            }\n          >\n            <span className=\"sr-only\">Decrease font weight</span>\n            <Minus className=\"h-5 w-5\" aria-hidden=\"true\" />\n          </button>\n          <div className=\"flex-center border-b border-t border-border bg-formDark px-4\">\n            <p className=\"text-[0.85rem] font-medium text-dark/80\">\n              {selectedText ? texts[selectedText - 1]?.style.fontWeight : 400}\n            </p>\n          </div>\n          <button\n            type=\"button\"\n            className=\"relative inline-flex items-center rounded-r-md bg-formDark px-2 py-2 text-dark ring-1 ring-inset ring-border focus:z-10 disabled:cursor-not-allowed\"\n            onClick={\n              selectedText && texts[selectedText - 1]?.style.fontWeight < 800\n                ? () =>\n                    handleFontWeight(\n                      texts[selectedText - 1]?.style.fontWeight + 100\n                    )\n                : () => handleFontWeight(800)\n            }\n          >\n            <span className=\"sr-only\">Increase font weight</span>\n            <Plus className=\"h-5 w-5\" aria-hidden=\"true\" />\n          </button>\n        </span>\n      </div>\n\n      <div className={`mt-8 flex flex-col gap-3 px-1`}>\n        <h1 className=\"text-[0.85rem]\">Color</h1>\n        <PopupColorPicker\n          color={\n            selectedText ? texts[selectedText - 1]?.style.textColor : '#fff'\n          }\n          onChange={handleColorChange}\n        />\n      </div>\n\n      <div className={`mt-8 flex flex-col gap-3 px-1 md:max-w-full`}>\n        <h1 className=\"text-[0.85rem]\">Letter spacing</h1>\n        <Slider\n          defaultValue={[0]}\n          max={0.2}\n          min={-0.05}\n          step={0.001}\n          value={\n            texts.length !== 0 && selectedText\n              ? [+texts[selectedText - 1]?.style.letterSpacing]\n              : [0]\n          }\n          onValueChange={(value: number[]) => {\n            setShowTextControls(false)\n            selectedText &&\n              setTexts(\n                texts.map((text, index) =>\n                  index === selectedText - 1\n                    ? {\n                        ...text,\n                        style: {\n                          ...text.style,\n                          letterSpacing: value[0],\n                        },\n                      }\n                    : text\n                )\n              )\n          }}\n          onValueCommit={() => setShowTextControls(true)}\n          onIncrement={() => {\n            if (!selectedText) return\n            if (\n              Number(texts[selectedText - 1]?.style.letterSpacing) >= 0.2 ||\n              texts.length === 0\n            )\n              return\n            setTexts(\n              texts.map((text, index) =>\n                index === selectedText - 1\n                  ? {\n                      ...text,\n                      style: {\n                        ...text.style,\n                        letterSpacing: Number(text.style.letterSpacing) + 0.001,\n                      },\n                    }\n                  : text\n              )\n            )\n          }}\n          onDecrement={() => {\n            if (!selectedText) return\n            if (\n              Number(texts[selectedText - 1]?.style.letterSpacing) <= -0.05 ||\n              texts.length === 0\n            )\n              return\n            setTexts(\n              texts.map((text, index) =>\n                index === selectedText - 1\n                  ? {\n                      ...text,\n                      style: {\n                        ...text.style,\n                        letterSpacing: Number(text.style.letterSpacing) - 0.001,\n                      },\n                    }\n                  : text\n              )\n            )\n          }}\n        />\n      </div>\n\n      <div className={`mt-8 flex flex-col gap-3 px-1`}>\n        <h1 className=\"text-[0.85rem]\">Alignment</h1>\n        <div className=\"flex flex-wrap gap-4\">\n          <TooltipProvider>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  size={'x-sm'}\n                  className=\"h-9 w-9 rounded-lg p-0\"\n                  variant=\"icon\"\n                  onClick={() => handleAlignmentChange('left')}\n                >\n                  <AlignLeft size={19} />\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent>\n                <p>Left</p>\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n\n          <TooltipProvider>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  size={'x-sm'}\n                  className=\"h-9 w-9 rounded-lg p-0\"\n                  variant=\"icon\"\n                  onClick={() => handleAlignmentChange('center')}\n                >\n                  <AlignHorizontalJustifyCenter size={19} />\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent>\n                <p>Center</p>\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n\n          <TooltipProvider>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  size={'x-sm'}\n                  className=\"h-9 w-9 rounded-lg p-0\"\n                  variant=\"icon\"\n                  onClick={() => handleAlignmentChange('right')}\n                >\n                  <AlignRight size={19} />\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent>\n                <p>Right</p>\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        </div>\n      </div>\n\n      <hr className=\"my-8\" />\n\n      <h3 className=\"flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n        <GalleryVerticalEnd className=\"rotate-90\" size={20} />\n        <span>Text shadow</span>\n      </h3>\n\n      <div className=\"mb-3 mt-8 flex items-center px-1\">\n        <h1 className=\"text-[0.85rem]\">Opacity</h1>\n        <p className=\"ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70\">\n          {Math.round(\n            Number(selectedText ? texts[selectedText - 1]?.style.shadowOpacity : 0.1) * 100\n          )}\n          %\n        </p>\n      </div>\n\n      <div className=\"flex gap-4 text-[0.85rem] md:max-w-full\">\n        <Slider\n          defaultValue={[0.1]}\n          min={0}\n          max={1}\n          step={0.01}\n          onValueChange={(value) => {\n            selectedText &&\n              setTexts(\n                texts.map((text, index) =>\n                  index === selectedText - 1\n                    ? {\n                        ...text,\n                        style: {\n                          ...text.style,\n                          shadowOpacity: value[0],\n                        },\n                      }\n                    : text\n                )\n              )\n          }}\n          value={\n            texts.length !== 0 && selectedText\n              ? [texts[selectedText - 1]?.style.shadowOpacity]\n              : [0.1]\n          }\n          onIncrement={() => {\n            if (!selectedText) return\n            if (\n              Number(texts[selectedText - 1]?.style.shadowOpacity) >= 1 ||\n              texts.length === 0\n            )\n              return\n            setTexts(\n              texts.map((text, index) =>\n                index === selectedText - 1\n                  ? {\n                      ...text,\n                      style: {\n                        ...text.style,\n                        shadowOpacity: Number(text.style.shadowOpacity) + 0.01,\n                      },\n                    }\n                  : text\n              )\n            )\n          }}\n          onDecrement={() => {\n            if (!selectedText) return\n            if (\n              Number(texts[selectedText - 1]?.style.shadowOpacity) <= 0 ||\n              texts.length === 0\n            )\n              return\n            setTexts(\n              texts.map((text, index) =>\n                index === selectedText - 1\n                  ? {\n                      ...text,\n                      style: {\n                        ...text.style,\n                        shadowOpacity: Number(text.style.shadowOpacity) - 0.01,\n                      },\n                    }\n                  : text\n              )\n            )\n          }}\n        />\n      </div>\n\n      <div className=\"mb-3 mt-8 flex items-center px-1\">\n        <h1 className=\"text-[0.85rem]\">Shadow color</h1>\n      </div>\n\n      <PopupColorPicker\n        shouldShowAlpha={false}\n        color={selectedText ? texts[selectedText - 1]?.style.shadowColor : '#333'}\n        onChange={handleShadowColorChange}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/text-options/index.tsx",
    "content": "import { Type } from 'lucide-react'\nimport AddTextLayer from './add-text-layer'\nimport FontSettings from './font-settings'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\n\nexport default function TextOptions() {\n  const { selectedText } = useSelectedLayers()\n\n  return (\n    <div className=\"flex w-full flex-col gap-8\">\n      <AddTextLayer />\n\n      <h3 className=\"flex items-center gap-2 text-xs font-medium uppercase text-dark/70\">\n        <Type size={20} />\n        <span>Appearance</span>\n      </h3>\n      <div className={`${selectedText ? '' : 'opacity-40 pointer-events-none'}`}>\n        <FontSettings />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/editor/tiptap-moveable.tsx",
    "content": "'use client'\n\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport { splitWidthHeight } from '@/utils/helper-fns'\nimport { useImageQualityStore } from '@/store/use-image-quality'\nimport {\n  Draggable,\n  DraggableProps,\n  Rotatable,\n  RotatableProps,\n  Scalable,\n  ScalableProps,\n  Snappable,\n  SnappableProps,\n  makeMoveable,\n} from 'react-moveable'\nimport React from 'react'\n\nconst Moveable = makeMoveable<\n  DraggableProps & ScalableProps & RotatableProps & SnappableProps\n  // @ts-ignore\n>([Draggable, Scalable, Rotatable, Snappable])\n\nexport default function TiptapMoveable({ id }: { id: string }) {\n  const { quality } = useImageQualityStore()\n  const { domResolution, scaleFactor, exactDomResolution } = useResizeCanvas()\n  const { images, texts } = useImageOptions()\n  const { selectedImage, selectedText } = useSelectedLayers()\n  const { width, height } = splitWidthHeight(exactDomResolution)\n  const moveableRef = React.useRef<any>(null)\n\n  const [domWidth, domHeight]: number[] = domResolution.split('x').map(Number)\n\n  const otherTexts = texts.filter((text) => text.id !== selectedText)\n  const elementGuidelines = [\n    ...images.map((image) => ({\n      element:\n        typeof document !== 'undefined'\n          ? document?.getElementById(`${image.id}`)\n          : '',\n    })),\n    ...otherTexts.map((text) => ({\n      element:\n        typeof document !== 'undefined'\n          ? document?.getElementById(`text-${text.id}`)\n          : '',\n    })),\n  ]\n\n  return (\n    <Moveable\n      ref={moveableRef}\n      target={\n        typeof document !== 'undefined' ? document?.getElementById(id) : ''\n      }\n      draggable={true}\n      onDrag={(e) => {\n        e.target.style.transform = e.transform\n        // const x = e.beforeTranslate[0]\n        // const y = e.beforeTranslate[1]\n        // // Calculate percentage values based on the parent dimensions\n        // const translateXPercent = (x / +width) * 100\n        // const translateYPercent = (y / +height) * 100\n        // // Apply the translate with percentage values\n        // e.target.style.transform = `translate(${translateXPercent}%, ${translateYPercent}%)`\n      }}\n      scalable={true}\n      keepRatio={true}\n      onScale={(e) => {\n        e.target.style.transform = e.drag.transform\n      }}\n      rotatable={true}\n      rotationPosition={'top'}\n      onRotate={(e) => {\n        e.target.style.transform = e.drag.transform\n      }}\n      snapRotationThreshold={5}\n      snapRotationDegrees={[0, 90, 180, 270]}\n      snappable={true}\n      snapDirections={{\n        top: true,\n        left: true,\n        bottom: true,\n        right: true,\n        center: true,\n        middle: true,\n      }}\n      snapThreshold={7}\n      horizontalGuidelines={[\n        domHeight / 2 / scaleFactor / quality,\n        domHeight / 1 / scaleFactor / quality,\n        0,\n      ]}\n      verticalGuidelines={[\n        domWidth / 2 / scaleFactor / quality,\n        domWidth / 1 / scaleFactor / quality,\n        0,\n      ]}\n      elementSnapDirections={{\n        top: true,\n        left: true,\n        bottom: true,\n        right: true,\n        center: true,\n        middle: true,\n      }}\n      elementGuidelines={elementGuidelines}\n    />\n  )\n}\n"
  },
  {
    "path": "components/editor/undo-redo-buttons.tsx",
    "content": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { useTemporalStore } from '@/store/use-image-options'\nimport { useMoveable } from '@/store/use-moveable'\nimport { Redo2, Undo2 } from 'lucide-react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nexport function useUndoRedoHotkeys() {\n  const { setShowControls } = useMoveable()\n  const { undo, redo } = useTemporalStore((state) => state)\n\n  useHotkeys(\n    'ctrl+z',\n    () => {\n      undo()\n      setShowControls(false)\n    },\n    [undo]\n  )\n\n  useHotkeys(\n    'ctrl+y',\n    () => {\n      redo()\n      setShowControls(false)\n    },\n    [redo]\n  )\n\n  return { undo, redo }\n}\n\nexport function UndoRedoButtons() {\n  const { undo, redo, futureStates, pastStates } = useTemporalStore(\n    (state) => state\n  )\n\n  return (\n    <div className=\"ml-auto flex\">\n      <Button\n        variant=\"outline\"\n        aria-label=\"undo\"\n        className=\"scale-75 rounded-md px-3 py-1\"\n        disabled={pastStates.length === 0}\n        onClick={() => undo()}\n      >\n        <Undo2 size={20} />\n      </Button>\n      <Button\n        variant=\"outline\"\n        aria-label=\"redo\"\n        className=\"scale-75 rounded-md px-3 py-1\"\n        disabled={futureStates.length === 0}\n        onClick={() => redo()}\n      >\n        <Redo2 size={20} />\n      </Button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/export-options.tsx",
    "content": "'use client'\n\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { toast } from '@/hooks/use-toast'\nimport { useImageOptions } from '@/store/use-image-options'\nimport { useImageQualityStore } from '@/store/use-image-quality'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport { qualities } from '@/utils/presets/qualities'\nimport { saveAs } from 'file-saver'\nimport * as htmlToImage from 'html-to-image'\nimport { Clipboard, Download } from 'lucide-react'\nimport { Button } from './ui/button'\n\ninterface ExportOptionsProps {\n  isLoggedIn: boolean\n}\n\nexport default function ExportOptions({ isLoggedIn }: ExportOptionsProps) {\n  const { quality, setQuality, fileType, setFileType } = useImageQualityStore()\n  const { images } = useImageOptions()\n  const { scaleFactor } = useResizeCanvas()\n\n  const checkDownloadPermission = () => {\n    if (images.length === 0) {\n      toast({\n        title: 'Error!',\n        description: 'Upload an image then try again',\n        variant: 'destructive',\n      })\n      return false\n    }\n    return true\n  }\n\n  const getHtmlToImageConfig = (element: HTMLElement, scale: number) => {\n    // Get device pixel ratio and reset it to 1 to avoid scaling issues\n    const originalPixelRatio = window.devicePixelRatio\n    Object.defineProperty(window, 'devicePixelRatio', {\n      get: function () {\n        return 1\n      },\n    })\n\n    const config = {\n      height: element.offsetHeight * scale,\n      width: element.offsetWidth * scale,\n      style: {\n        transform: `scale(${scale})`,\n        transformOrigin: 'top left',\n        width: `${element.offsetWidth}px`,\n        height: `${element.offsetHeight}px`,\n      },\n      pixelRatio: 1, // Force pixel ratio to 1\n    }\n\n    // Restore original device pixel ratio after config is created\n    Object.defineProperty(window, 'devicePixelRatio', {\n      get: function () {\n        return originalPixelRatio\n      },\n    })\n\n    return config\n  }\n\n  const handleExportError = (error: any) => {\n    toast({\n      title: 'Error!',\n      description: error.message,\n      variant: 'destructive',\n    })\n    return Promise.reject(error)\n  }\n\n  const createSnapshot = async () => {\n    if (!checkDownloadPermission()) return\n\n    const scale = scaleFactor * quality\n    const element = document?.getElementById('canvas-container')\n    if (!element) throw new Error('Canvas element not found')\n\n    const config = getHtmlToImageConfig(element, scale)\n\n    try {\n      let result: Blob | string\n      switch (fileType) {\n        case 'JPG':\n          result = await htmlToImage.toJpeg(element, config)\n          break\n        case 'PNG':\n          result = (await htmlToImage.toBlob(element, config)) as Blob\n          break\n        case 'WEBP':\n          const canvas = await htmlToImage.toCanvas(element, config)\n          const webpUrl = canvas.toDataURL('image/webp')\n          const a = document.createElement('a')\n          a.href = webpUrl\n          a.download = `prismify-render-${Date.now()}.webp`\n          document.body.appendChild(a)\n          a.click()\n          document.body.removeChild(a)\n          return\n        case 'SVG':\n          result = await htmlToImage.toSvg(element, config)\n          break\n        default:\n          throw new Error('Unsupported file type')\n      }\n\n      localStorage.setItem('downloaded', 'true')\n      return result as Blob\n    } catch (error) {\n      return handleExportError(error)\n    }\n  }\n\n  const copyToClipboard = async () => {\n    const isFirefox = navigator.userAgent.includes('Firefox')\n\n    if (!checkDownloadPermission()) return\n\n    if (isFirefox) {\n      return toast({\n        title: \"Couldn't copy image\",\n        description: \"Firefox doesn't support it\",\n        variant: 'destructive',\n      })\n    }\n\n    try {\n      const blob = await createSnapshot()\n      if (!blob) return\n      const clipboardItem = new ClipboardItem({\n        'image/png': Promise.resolve(new Blob([blob], { type: 'image/png' })),\n      })\n\n      await navigator.clipboard.write([clipboardItem])\n      toast({ title: 'Copied to clipboard. 🥳' })\n    } catch (error: any) {\n      toast({\n        title: \"Couldn't copy\",\n        description: error.message,\n        variant: 'destructive',\n      })\n    }\n  }\n\n  const handleDownload = async () => {\n    try {\n      const blob = await createSnapshot()\n      if (blob) {\n        saveAs(blob, `prismify-render-${Date.now()}`)\n      }\n    } catch (error: any) {\n      toast({\n        variant: 'destructive',\n        title: 'Error!',\n        description: error.message,\n      })\n    }\n  }\n\n  return (\n    <>\n      <Button\n        className=\"w-fit text-[0.8rem] font-medium\"\n        variant=\"icon\"\n        size=\"sm\"\n        onClick={copyToClipboard}\n      >\n        <Clipboard size={18} className=\"mr-0 text-dark/80 lg:mr-2\" />\n        <p className=\"hidden text-dark/80 lg:block\">Copy</p>\n      </Button>\n\n      <div\n        tabIndex={0}\n        className=\"z-50 flex h-fit overflow-hidden rounded-xl border border-[rgba(99,102,241,0.15)] focus:outline-none focus:ring-2 focus:ring-[#898aeb]\"\n        onKeyDown={(e) => e.key === 'Enter' && handleDownload()}\n      >\n        <Button\n          tabIndex={-1}\n          onClick={handleDownload}\n          className=\"rounded-none border-b-0 border-l-0 border-r-[1.5px] border-t-0 border-[rgba(99,102,241,0.15)] px-2 text-[0.8rem] font-medium\"\n          variant=\"stylish\"\n          size=\"sm\"\n        >\n          <Download size={18} className=\"mr-0 lg:mr-2\" />\n          <p className=\"hidden font-medium lg:block\">Save</p>\n        </Button>\n\n        <Popover>\n          <PopoverTrigger asChild>\n            <Button\n              tabIndex={-1}\n              className=\"rounded-none border-b-0 border-l-0 border-r-[1.5px] border-t-0 border-[rgba(99,102,241,0.15)] px-2.5 text-[0.8rem] font-medium\"\n              variant=\"stylish\"\n              size=\"sm\"\n            >\n              {quality}x\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent\n            align=\"end\"\n            className=\"mb-4 flex w-full min-w-[15rem] max-w-[15rem] flex-col gap-4 rounded-lg border border-border/60 dark:bg-[#151515]\"\n          >\n            <p className=\"text-sm font-medium text-dark/90\">Quality</p>\n            <div className=\"grid w-full grid-cols-2 gap-2.5\">\n              {qualities.map((q) => (\n                <Button\n                  variant={q.value === quality ? 'stylish' : 'outline'}\n                  key={q.quality}\n                  onClick={() => setQuality(q.value)}\n                  className=\"rounded-lg border border-border/80 text-[0.8rem] font-medium\"\n                >\n                  {q.quality.split('x')[0]}x &mdash;{' '}\n                  <span className={q.value === quality ? '' : 'text-dark/60'}>\n                    {q.quality.split('x')[1]}\n                  </span>\n                </Button>\n              ))}\n            </div>\n          </PopoverContent>\n        </Popover>\n\n        <Popover>\n          <PopoverTrigger asChild>\n            <Button\n              tabIndex={-1}\n              className=\"flex items-center justify-center rounded-none border-none px-2 text-[0.8rem] font-medium\"\n              variant=\"stylish\"\n              size=\"sm\"\n            >\n              {fileType}\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent\n            align=\"end\"\n            className=\"mb-4 flex w-full min-w-[15rem] max-w-[15rem] flex-col gap-4 rounded-lg border border-border/60 dark:bg-[#151515]\"\n          >\n            <p className=\"text-sm font-medium text-dark/90\">Image formats</p>\n            <div className=\"grid w-full grid-cols-2 gap-2.5\">\n              {(['PNG', 'JPG', 'WEBP', 'SVG'] as const).map((format) => (\n                <Button\n                  variant={format === fileType ? 'stylish' : 'outline'}\n                  key={format}\n                  onClick={() => setFileType(format)}\n                  className=\"rounded-lg border border-border/80 text-[0.8rem] font-medium\"\n                >\n                  .{format}\n                </Button>\n              ))}\n            </div>\n          </PopoverContent>\n        </Popover>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/footer.tsx",
    "content": "import { cn } from '@/utils/button-utils'\nimport Link from 'next/link'\nimport React from 'react'\nimport { buttonVariants } from './ui/button'\n\nexport default function Footer() {\n  return (\n    <footer className=\"mt-16 w-full border-t border-border/40 bg-black/10 px-4 py-10 sm:px-6 lg:px-8\">\n      <div className=\"grid grid-cols-1 items-center gap-5 text-center md:grid-cols-3 md:text-start\">\n        <div>\n          <Link\n            href=\"/\"\n            className={cn(\n              buttonVariants({ variant: 'noHoverGhost' }),\n              ' inline-flex gap-2 text-xl font-normal text-dark/80 focus:outline-none focus:ring-1 focus:ring-gray-600'\n            )}\n            aria-label=\"Prismify\"\n          >\n            <p className=\"mr-3 text-dark/80 \">Prismify</p>\n          </Link>\n        </div>\n\n        <ul className=\"text-center\">\n          <li className=\"relative inline-block pe-8 before:absolute before:end-3 before:top-1/2 before:-translate-y-1/2 before:content-['/'] last:pe-0 last-of-type:before:hidden\">\n            <Link\n              prefetch={false}\n              className=\"inline-flex gap-x-2 text-sm text-dark/70 hover:text-dark focus:outline-none focus:ring-1 focus:ring-[#898aeb]\"\n              href=\"/about\"\n            >\n              About\n            </Link>\n          </li>\n          <li className=\"relative inline-block pe-8 before:absolute before:end-3 before:top-1/2 before:-translate-y-1/2 before:content-['/'] last:pe-0 last-of-type:before:hidden\">\n            <Link\n              prefetch={false}\n              className=\"inline-flex gap-x-2 text-sm text-dark/70 hover:text-dark focus:outline-none focus:ring-1 focus:ring-[#898aeb]\"\n              href=\"/privacy-policy\"\n            >\n              Privacy policy\n            </Link>\n          </li>\n          <li className=\"relative inline-block pe-8 before:absolute before:end-3 before:top-1/2 before:-translate-y-1/2 before:content-['/'] last:pe-0 last-of-type:before:hidden\">\n            <Link\n              prefetch={false}\n              className=\"inline-flex gap-x-2 text-sm text-dark/70 hover:text-dark focus:outline-none focus:ring-1 focus:ring-[#898aeb]\"\n              href=\"/terms-of-use\"\n            >\n              Terms of use\n            </Link>\n          </li>\n        </ul>\n\n        <div className=\"space-x-2 md:text-end\">\n          <a\n            className=\"inline-flex h-8 w-8 items-center justify-center gap-x-2 rounded-full border border-transparent text-sm font-semibold text-dark/70 hover:bg-white/20 hover:text-dark focus:outline-none focus:ring-1 focus:ring-[#898aeb] disabled:pointer-events-none disabled:opacity-50\"\n            href=\"#\"\n          >\n            <svg\n              className=\"h-3.5 w-3.5 flex-shrink-0\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"16\"\n              height=\"16\"\n              fill=\"currentColor\"\n              viewBox=\"0 0 16 16\"\n            >\n              <path d=\"M15.545 6.558a9.42 9.42 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.689 7.689 0 0 1 5.352 2.082l-2.284 2.284A4.347 4.347 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.792 4.792 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.702 3.702 0 0 0 1.599-2.431H8v-3.08h7.545z\" />\n            </svg>\n          </a>\n          <a\n            className=\"inline-flex h-8 w-8 items-center justify-center gap-x-2 rounded-full border border-transparent text-sm font-semibold text-dark/70 hover:bg-white/20 hover:text-dark focus:outline-none focus:ring-1 focus:ring-[#898aeb] disabled:pointer-events-none disabled:opacity-50\"\n            href=\"#\"\n          >\n            <svg\n              className=\"h-3.5 w-3.5 flex-shrink-0\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"16\"\n              height=\"16\"\n              fill=\"currentColor\"\n              viewBox=\"0 0 16 16\"\n            >\n              <path d=\"M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z\" />\n            </svg>\n          </a>\n          <a\n            className=\"inline-flex h-8 w-8 items-center justify-center gap-x-2 rounded-full border border-transparent text-sm font-semibold text-dark/70 hover:bg-white/20 hover:text-dark focus:outline-none focus:ring-1 focus:ring-[#898aeb] disabled:pointer-events-none disabled:opacity-50\"\n            href=\"#\"\n          >\n            <svg\n              className=\"h-3.5 w-3.5 flex-shrink-0\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"16\"\n              height=\"16\"\n              fill=\"currentColor\"\n              viewBox=\"0 0 16 16\"\n            >\n              <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z\" />\n            </svg>\n          </a>\n          <a\n            className=\"inline-flex h-8 w-8 items-center justify-center gap-x-2 rounded-full border border-transparent text-sm font-semibold text-dark/70 hover:bg-white/20 hover:text-dark focus:outline-none focus:ring-1 focus:ring-[#898aeb] disabled:pointer-events-none disabled:opacity-50\"\n            href=\"#\"\n          >\n            <svg\n              className=\"h-3.5 w-3.5 flex-shrink-0\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"16\"\n              height=\"16\"\n              fill=\"currentColor\"\n              viewBox=\"0 0 16 16\"\n            >\n              <path d=\"M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z\" />\n            </svg>\n          </a>\n        </div>\n      </div>\n    </footer>\n  )\n}\n"
  },
  {
    "path": "components/icons/index.tsx",
    "content": "import InfoIcon from './info.icon'\n\ntype IconType = {\n  name: 'info'\n  size?: number\n  color?: string\n  className?: string\n  strokeWidth?: number\n  variant?: 'default' | 'solid' | 'duotone'\n}\n\nexport default function Icon({\n  name,\n  size,\n  color,\n  className,\n  strokeWidth,\n  variant,\n}: IconType) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size || '24'}\n      height={size || '24'}\n      className={`inline-block ${className}`}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color || 'currentColor'}\n      strokeWidth={strokeWidth || '2'}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      {\n        {\n          info: <InfoIcon variant={variant} />,\n        }[name]\n      }\n    </svg>\n  )\n}\n"
  },
  {
    "path": "components/icons/info.icon.tsx",
    "content": "import React from 'react'\n\nexport default function InfoIcon({\n  variant,\n}: {\n  variant?: 'default' | 'solid' | 'duotone'\n}) {\n  return {\n    default: (\n      <>\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M9 4.625C9.41421 4.625 9.75 4.96079 9.75 5.375V9.375C9.75 9.78921 9.41421 10.125 9 10.125C8.58579 10.125 8.25 9.78921 8.25 9.375V5.375C8.25 4.96079 8.58579 4.625 9 4.625Z\"\n        />\n        <path d=\"M10 12.375C10 12.9273 9.55229 13.375 9 13.375C8.44771 13.375 8 12.9273 8 12.375C8 11.8227 8.44771 11.375 9 11.375C9.55229 11.375 10 11.8227 10 12.375Z\" />\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M9 1.38462C4.79414 1.38462 1.38462 4.79414 1.38462 9C1.38462 13.2059 4.79414 16.6154 9 16.6154C13.2059 16.6154 16.6154 13.2059 16.6154 9C16.6154 4.79414 13.2059 1.38462 9 1.38462ZM0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9Z\"\n        />\n      </>\n    ),\n    solid: (\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9ZM9.75 5.5C9.75 5.08579 9.41421 4.75 9 4.75C8.58579 4.75 8.25 5.08579 8.25 5.5V9.5C8.25 9.91421 8.58579 10.25 9 10.25C9.41421 10.25 9.75 9.91421 9.75 9.5V5.5ZM9 13.5C9.55229 13.5 10 13.0523 10 12.5C10 11.9477 9.55229 11.5 9 11.5C8.44771 11.5 8 11.9477 8 12.5C8 13.0523 8.44771 13.5 9 13.5Z\"\n      />\n    ),\n    duotone: (\n      <>\n        <g id=\"Style=Duotone\">\n          <path\n            id=\"shape\"\n            d=\"M10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10C19 5.02944 14.9706 1 10 1Z\"\n            fill=\"#737373\"\n            fillOpacity=\"1\"\n          />\n          <g id=\"shape_2\">\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M10 5.625C10.4142 5.625 10.75 5.96079 10.75 6.375L10.75 10.375C10.75 10.7892 10.4142 11.125 10 11.125C9.58579 11.125 9.25 10.7892 9.25 10.375L9.25 6.375C9.25 5.96079 9.58579 5.625 10 5.625Z\"\n              fill=\"#201F29\"\n            />\n            <path\n              d=\"M11 13.375C11 13.9273 10.5523 14.375 10 14.375C9.44772 14.375 9 13.9273 9 13.375C9 12.8227 9.44772 12.375 10 12.375C10.5523 12.375 11 12.8227 11 13.375Z\"\n              fill=\"#201F29\"\n            />\n          </g>\n        </g>\n      </>\n    ),\n  }[variant || 'default']\n}\n"
  },
  {
    "path": "components/loader.tsx",
    "content": "// Reusable loading spinner component to be used as suspense fallback on server components.\n\nexport default function Loader() {\n  return (\n    <div className=\"flex-center overflow-x-hidden h-screen w-screen\">\n      <svg\n        width=\"90\"\n        height=\"90\"\n        viewBox=\"0 0 45 45\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        stroke=\"#898aeb\"\n      >\n        <g fill=\"none\" fillRule=\"evenodd\" strokeWidth=\"2\">\n          <circle cx=\"22\" cy=\"22\" r=\"1\">\n            <animate\n              attributeName=\"r\"\n              begin=\"0s\"\n              dur=\"1.8s\"\n              values=\"1; 20\"\n              calcMode=\"spline\"\n              keyTimes=\"0; 1\"\n              keySplines=\"0.165, 0.84, 0.44, 1\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"stroke-opacity\"\n              begin=\"0s\"\n              dur=\"1.8s\"\n              values=\"1; 0\"\n              calcMode=\"spline\"\n              keyTimes=\"0; 1\"\n              keySplines=\"0.3, 0.61, 0.355, 1\"\n              repeatCount=\"indefinite\"\n            />\n          </circle>\n          <circle cx=\"22\" cy=\"22\" r=\"1\">\n            <animate\n              attributeName=\"r\"\n              begin=\"-0.9s\"\n              dur=\"1.8s\"\n              values=\"1; 20\"\n              calcMode=\"spline\"\n              keyTimes=\"0; 1\"\n              keySplines=\"0.165, 0.84, 0.44, 1\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"stroke-opacity\"\n              begin=\"-0.9s\"\n              dur=\"1.8s\"\n              values=\"1; 0\"\n              calcMode=\"spline\"\n              keyTimes=\"0; 1\"\n              keySplines=\"0.3, 0.61, 0.355, 1\"\n              repeatCount=\"indefinite\"\n            />\n          </circle>\n        </g>\n      </svg>\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/navbar.tsx",
    "content": "'use client'\n\nimport { AuthModal } from '@/components/auth-modal'\nimport ExportOptions from '@/components/export-options'\nimport { NavLinks } from '@/components/navlinks'\nimport { usePathname } from 'next/navigation'\nimport { UserDropDown } from '@/components/user-dropdown'\nimport { useSession } from 'next-auth/react'\n\nexport default function Navbar() {\n  const pathname = usePathname()\n  const isHome = pathname === '/'\n\n  return (\n    <header className=\"fixed top-0 z-[10] flex h-[72px] w-screen items-center border-b border-border bg-[#131313] py-4 pr-4 pt-4 backdrop-blur-md \">\n      <div className=\"container flex w-full items-center justify-between\">\n        <NavLinks />\n        <div className=\"flex items-center gap-10 \">\n          <UserSection isHome={isHome} />\n        </div>\n      </div>\n    </header>\n  )\n}\n\nconst UserSection = ({ isHome }: { isHome: boolean }) => {\n  const { data, status } = useSession()\n\n  if (status === 'loading') return null\n\n  return (\n    <div className=\"flex items-center gap-2\">\n      {isHome && <ExportOptions isLoggedIn={!!data?.user || false} />}\n\n      {isHome && <div className=\"bg-border-dark mx-3 h-7 w-[2px] bg-border\" />}\n\n      {!!data?.user ? (\n        <UserDropDown\n          img={data?.user?.image || '/images/fallback.jpg'}\n          username={data?.user?.name || 'User'}\n          isCreator={data?.user.isCreator ?? false}\n        />\n      ) : (\n        <AuthModal />\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "components/navlinks.tsx",
    "content": "// Links in the navbar\n\nimport { Badge } from '@/components/ui/badge'\nimport { buttonVariants } from '@/components/ui/button'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { GradientText } from '@/components/ui/gradient-text'\nimport { cn } from '@/utils/button-utils'\nimport { BadgeInfo, BookCopy, ChevronDown, type LucideIcon } from 'lucide-react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport React from 'react'\n\nconst menuItems: Array<{\n  href: string\n  icon: LucideIcon\n  label: string\n  separateFromHere?: boolean\n}> = [\n  { href: '/articles', icon: BookCopy, label: 'Articles' },\n  // , separateFromHere: true\n  { href: '/about', icon: BadgeInfo, label: 'About' },\n  // { href: '/contact', icon: Mails, label: 'Contact' },\n]\n\nexport function NavLinks() {\n  return (\n    <nav className=\"flex items-center gap-8\">\n      {/* Can add hamburger or something here */}\n      <Link\n        href=\"/\"\n        className={cn(\n          buttonVariants({ variant: 'noHoverGhost' }),\n          ' inline-flex items-center gap-3 px-0'\n        )}\n      >\n        <Image\n          src=\"/images/image.webp\"\n          width={35}\n          height={35}\n          quality={80}\n          alt=\"prismify logo\"\n          priority\n        />\n        <GradientText\n          as=\"h1\"\n          className=\"mr-3 hidden text-lg font-semibold tracking-tight md:block\"\n        >\n          Prismify\n          <Badge className=\"ml-2\">Beta</Badge>\n        </GradientText>\n\n        <div className=\"hidden h-5 w-[1.5px] bg-border md:block\" />\n      </Link>\n\n      <DropdownMenu>\n        <DropdownMenuTrigger className=\"group hidden items-center focus-visible:outline-none md:flex\">\n          <p className=\"text-sm font-medium text-dark/70 group-hover:text-dark/90\">\n            Resources\n          </p>\n          <ChevronDown\n            size={16}\n            className=\"ml-1 translate-y-0.5 text-dark/70 group-hover:text-dark/90\"\n          />\n        </DropdownMenuTrigger>\n        <DropdownMenuContent\n          sideOffset={15}\n          className=\"w-[180px] rounded-xl border border-border/70 bg-[#151515]/95 p-1.5 py-2 shadow-xl backdrop-blur-lg\"\n        >\n          <DropdownMenuGroup>\n            {menuItems.map((item, index) => (\n              <React.Fragment key={item.label}>\n                <DropdownMenuItem\n                  asChild\n                  className={`group cursor-pointer rounded-lg focus:bg-white ${\n                    index !== menuItems.length - 1 ? 'mb-1' : ''\n                  }`}\n                  key={item.href + index}\n                >\n                  <Link\n                    href={item.href}\n                    className=\"flex w-full items-center focus:shadow-md\"\n                  >\n                    <item.icon\n                      size={18}\n                      className=\"mr-3 h-4 w-4  text-dark/80 group-focus:text-black/90\"\n                    />\n                    <span className=\"font-medium group-focus:text-black/90\">\n                      {item.label}\n                    </span>\n                  </Link>\n                </DropdownMenuItem>\n                {item.separateFromHere && (\n                  <DropdownMenuSeparator\n                    key={item.label}\n                    className=\"mb-1 opacity-80\"\n                  />\n                )}\n              </React.Fragment>\n            ))}\n          </DropdownMenuGroup>\n        </DropdownMenuContent>\n      </DropdownMenu>\n\n      <Link\n        target=\"_blank\"\n        className=\"hidden md:flex\"\n        href=\"https://x.com/sls0n\"\n      >\n        <p className=\"text-sm font-medium text-dark/70\">Contact</p>\n      </Link>\n    </nav>\n  )\n}\n"
  },
  {
    "path": "components/popup-color-picker.tsx",
    "content": "// Formatted color picker component which opens a popover with a color picker.\n\nimport { Popover, PopoverContent, PopoverTrigger } from './ui/popover'\nimport { ChevronDown } from 'lucide-react'\nimport ColorPicker from '@/components/color-picker'\n\nexport default function PopupColorPicker({\n  onChange,\n  color,\n  shouldShowAlpha = true,\n  shouldShowDropdown = true,\n}: {\n  onChange: (color: string) => void\n  color: string\n  shouldShowAlpha?: boolean\n  shouldShowDropdown?: boolean\n}) {\n  return (\n    <Popover>\n      <PopoverTrigger>\n        {shouldShowDropdown ? (\n          <div className=\"flex h-14 w-24 rounded-xl bg-formDark\">\n            <div className=\"ml-4 flex h-full basis-[70%] items-center\">\n              <div\n                className=\"flex h-[55%] w-[70%] rounded-md bg-sidebar\"\n                style={{ background: `${color}` }}\n              ></div>\n            </div>\n            <div className=\"mr-4 flex flex-1 items-center\">\n              <ChevronDown\n                size={20}\n                className=\"text-dark/80\"\n              />\n            </div>\n          </div>\n        ) : (\n          <div\n            className=\"flex h-8 w-10 rounded-md border border-border \"\n            style={{ background: `${color}` }}\n          ></div>\n        )}\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        className=\"flex-center w-fit flex-wrap gap-3\"\n      >\n        <ColorPicker\n          shouldShowAlpha={shouldShowAlpha}\n          colorState={color}\n          onChange={(color) => {\n            onChange(color)\n          }}\n        />\n      </PopoverContent>\n    </Popover>\n  )\n}\n"
  },
  {
    "path": "components/profile-dialog.tsx",
    "content": "'use client'\n\nimport { useMemo } from 'react'\nimport { useSession } from 'next-auth/react'\nimport { User as UserIcon } from 'lucide-react'\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport {\n  Drawer,\n  DrawerContent,\n  DrawerHeader,\n  DrawerTitle,\n} from '@/components/ui/drawer'\nimport { useMediaQuery } from '@/hooks/use-media-query'\nimport { formatDate } from '@/utils/helper-fns'\n\ninterface ProfileDialogProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\nexport default function ProfileDialog({\n  open,\n  onOpenChange,\n}: ProfileDialogProps) {\n  const isDesktop = useMediaQuery('(min-width: 768px)')\n  const { data } = useSession()\n\n  const name = data?.user?.name ?? 'User'\n  const image = data?.user?.image ?? ''\n  const joined = useMemo(() => {\n    if (!data?.user?.createdAt) return null\n    return formatDate(data.user.createdAt)\n  }, [data?.user?.createdAt])\n\n  const initials = useMemo(() => {\n    return name\n      .split(' ')\n      .map((n) => n.charAt(0))\n      .join('')\n      .slice(0, 2)\n      .toUpperCase()\n  }, [name])\n\n  const avatar = image ? (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img\n      src={image}\n      alt={name}\n      width={96}\n      height={96}\n      className=\"h-24 w-24 rounded-full object-cover shadow-lg\"\n    />\n  ) : (\n    <div className=\"flex h-24 w-24 items-center justify-center rounded-full bg-muted text-2xl font-semibold text-foreground shadow-inner\">\n      {initials}\n    </div>\n  )\n\n  const Body = (\n    <div className=\"flex flex-col items-center space-y-3\">\n      {avatar}\n      <p className=\"text-lg font-semibold text-foreground\">{name}</p>\n      {joined && (\n        <p className=\"text-sm text-muted-foreground\">Joined {joined}</p>\n      )}\n    </div>\n  )\n\n  if (isDesktop) {\n    return (\n      <Dialog open={open} onOpenChange={onOpenChange}>\n        <DialogContent className=\"px-6 py-12\">{Body}</DialogContent>\n      </Dialog>\n    )\n  }\n\n  return (\n    <Drawer open={open} onOpenChange={onOpenChange}>\n      <DrawerContent className=\"mb-6 rounded-xl bg-background px-6 py-4\">\n        <DrawerHeader className=\"text-left\">\n          <DrawerTitle className=\"mb-4 flex items-center gap-1.5\">\n            <UserIcon size={18} className=\"opacity-80\" />\n            <span>Profile</span>\n          </DrawerTitle>\n        </DrawerHeader>\n        {Body}\n        <div className=\"mt-4\" />\n      </DrawerContent>\n    </Drawer>\n  )\n}\n"
  },
  {
    "path": "components/settings-dialog.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useRouter } from 'next/navigation'\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport {\n  Drawer,\n  DrawerContent,\n  DrawerHeader,\n  DrawerTitle,\n} from '@/components/ui/drawer'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { useMediaQuery } from '@/hooks/use-media-query'\nimport { toast } from '@/hooks/use-toast'\nimport { useSession } from 'next-auth/react'\nimport {\n  Image as ImageIcon,\n  Save,\n  Settings as SettingsIcon,\n  User as UserIcon,\n  Loader2,\n} from 'lucide-react'\n\ninterface SettingsDialogProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\nexport default function SettingsDialog({\n  open,\n  onOpenChange,\n}: SettingsDialogProps) {\n  const isDesktop = useMediaQuery('(min-width: 768px)')\n  const router = useRouter()\n  const { data: session, update } = useSession()\n  const [name, setName] = useState(session?.user?.name || '')\n  const [image, setImage] = useState(session?.user?.image || '')\n  const [isLoading, setIsLoading] = useState(false)\n\n  useEffect(() => {\n    setName(session?.user?.name || '')\n    setImage(session?.user?.image || '')\n  }, [session])\n\n  const onSubmit = async (e: React.FormEvent) => {\n    e.preventDefault()\n    setIsLoading(true)\n\n    try {\n      const payload = {\n        name: name.trim() || undefined,\n        image: image.trim() || undefined,\n      }\n\n      const res = await fetch('/api/user/settings', {\n        method: 'PATCH',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(payload),\n      })\n\n      if (!res.ok) {\n        const errorText = await res.text()\n        throw new Error(errorText)\n      }\n\n      // force a session update with the new data\n      await update({\n        name: payload.name,\n        image: payload.image,\n      })\n\n      router.refresh()\n      toast({ title: 'Profile updated!' })\n      onOpenChange(false)\n    } catch (error: any) {\n      toast({\n        title: 'Error updating profile',\n        description: error.message,\n        variant: 'destructive',\n      })\n    } finally {\n      setIsLoading(false)\n    }\n  }\n\n  const Form = (\n    <form onSubmit={onSubmit} className=\"space-y-4\">\n      <div className=\"space-y-1\">\n        <label htmlFor=\"name\" className=\"text-sm font-medium\">\n          Username\n        </label>\n        <div className=\"relative\">\n          <Input\n            id=\"name\"\n            className=\"pl-9\"\n            value={name}\n            onChange={(e) => setName(e.target.value)}\n            disabled={isLoading}\n          />\n          <UserIcon\n            size={16}\n            className=\"absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground\"\n          />\n        </div>\n      </div>\n      <div className=\"space-y-1\">\n        <label htmlFor=\"image\" className=\"text-sm font-medium\">\n          Image URL\n        </label>\n        <div className=\"relative\">\n          <Input\n            id=\"image\"\n            className=\"pl-9\"\n            value={image}\n            onChange={(e) => setImage(e.target.value)}\n            placeholder=\"https://example.com/image.jpg\"\n            disabled={isLoading}\n          />\n          {image ? (\n            <img\n              src={image}\n              alt=\"Profile preview\"\n              className=\"absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 rounded-full border object-cover\"\n            />\n          ) : (\n            <ImageIcon\n              size={16}\n              className=\"absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground\"\n            />\n          )}\n        </div>\n      </div>\n      <Button\n        type=\"submit\"\n        className=\"flex w-full items-center justify-center gap-2\"\n        disabled={isLoading}\n      >\n        {isLoading ? (\n          <>\n            <Loader2 size={16} className=\"animate-spin\" />\n            <span>Saving...</span>\n          </>\n        ) : (\n          <>\n            <Save size={16} />\n            <span>Save</span>\n          </>\n        )}\n      </Button>\n    </form>\n  )\n\n  if (isDesktop) {\n    return (\n      <Dialog open={open} onOpenChange={onOpenChange}>\n        <DialogContent className=\"p-6\">\n          <DialogHeader>\n            <DialogTitle className=\"mb-4 flex items-center gap-1.5\">\n              <SettingsIcon size={18} className=\"opacity-80\" />\n              <span>Edit profile</span>\n            </DialogTitle>\n          </DialogHeader>\n          {Form}\n        </DialogContent>\n      </Dialog>\n    )\n  }\n\n  return (\n    <Drawer open={open} onOpenChange={onOpenChange}>\n      <DrawerContent className=\"mb-6 rounded-xl bg-background px-6 py-4\">\n        <DrawerHeader className=\"text-left\">\n          <DrawerTitle className=\"mb-4 flex items-center gap-1.5\">\n            <SettingsIcon size={18} className=\"opacity-80\" />\n            <span>Edit profile</span>\n          </DrawerTitle>\n        </DrawerHeader>\n        {Form}\n        <div className=\"mt-4\" />\n      </DrawerContent>\n    </Drawer>\n  )\n}\n"
  },
  {
    "path": "components/sign-in-form.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport { useState } from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { GradientText } from '@/components/ui/gradient-text'\nimport { toast } from '@/hooks/use-toast'\nimport { signIn } from 'next-auth/react'\nimport { ArrowRight } from 'lucide-react'\n\ntype SignInFormProps = {\n  authenticated?: boolean\n}\n\nexport default function SignInForm({ authenticated }: SignInFormProps) {\n  const [isGoogleLoading, setIsGoogleLoading] = useState(false)\n  const [isGithubLoading, setIsGithubLoading] = useState(false)\n\n  const signInWithProvider = (provider: 'google' | 'github') => {\n    provider === 'google' ? setIsGoogleLoading(true) : setIsGithubLoading(true)\n    signIn(provider, {\n      callbackUrl: '/',\n    })\n      .then((data) => {\n        console.log(data)\n      })\n      .catch((err) => {\n        console.error(err)\n        toast({\n          variant: 'destructive',\n          title: 'Trouble signing in!',\n          description: err.message ?? 'Something went wrong',\n        })\n      })\n      .finally(() => {\n        setIsGoogleLoading(false)\n        setIsGithubLoading(false)\n      })\n  }\n\n  if (authenticated) return null\n\n  return (\n    <div className=\"flex items-center justify-center\">\n      <div className=\"w-full space-y-4\">\n        <h1 className=\"text-center text-4xl font-semibold text-dark sm:font-bold\">\n          Sign in to{' '}\n          <GradientText variant=\"purple\" className=\"font-bold\">\n            Prismify\n          </GradientText>\n        </h1>\n        {/* Continue with separator */}\n        <div className=\"relative\">\n          <div className=\"absolute inset-0 flex items-center\">\n            <div className=\"w-full border-t border-[#22262b]\" />\n          </div>\n          <div className=\"relative flex justify-center rounded-md text-sm\">\n            <span className=\"bg-[#121212] px-2 text-dark/60\">\n              Click on a provider to sign in\n            </span>\n          </div>\n        </div>\n\n        {/* Provider buttons */}\n        <div className=\"flex flex-col gap-y-5 pt-8\">\n          <ProviderButton\n            provider=\"google\"\n            isLoading={isGoogleLoading}\n            onClick={() => signInWithProvider('google')}\n          />\n          <ProviderButton\n            provider=\"github\"\n            isLoading={isGithubLoading}\n            onClick={() => signInWithProvider('github')}\n          />\n        </div>\n      </div>\n    </div>\n  )\n}\n\nconst ProviderButton = ({\n  provider,\n  isLoading,\n  onClick,\n}: {\n  provider: 'google' | 'github'\n  isLoading: boolean\n  onClick: () => void\n}) => {\n  return (\n    <Button\n      variant=\"outline\"\n      className=\"group flex items-center gap-5 rounded-md border-[#222]/50 bg-[#181818] px-4 shadow-sm transition-all\n      duration-300 ease-in-out hover:border-[#212121] hover:bg-[#191919] hover:shadow-md\n      \"\n      size={'lg'}\n      type=\"button\"\n      onClick={onClick}\n      disabled={isLoading}\n    >\n      <img\n        className=\"h-5 w-5\"\n        src={\n          provider === 'google'\n            ? `https://img.icons8.com/color/48/000000/google-logo.png`\n            : `https://img.icons8.com/fluency/48/000000/github.png`\n        }\n        alt={`${provider} logo`}\n        loading=\"lazy\"\n      />\n      <span className=\"text-[0.9rem] text-dark\">\n        Continue with {provider === 'google' ? 'Google' : 'GitHub'}\n      </span>\n      <ArrowRight size={16} className=\"text-dark/50\" />\n    </Button>\n  )\n}\n"
  },
  {
    "path": "components/spinner/spinner.module.css",
    "content": ".ring {\n  --uib-size: 25px;\n  --uib-speed: 2s;\n  --uib-color: #fefefe;\n  \n  height: var(--uib-size);\n  width: var(--uib-size);\n  vertical-align: middle;\n  transform-origin: center;\n  animation: rotate var(--uib-speed) linear infinite;\n}\n\n.ring circle {\n  fill: none;\n  stroke: var(--uib-color);\n  stroke-dasharray: 1, 200;\n  stroke-dashoffset: 0;\n  stroke-linecap: round;\n  animation: stretch calc(var(--uib-speed) * 0.75) ease-in-out infinite;\n}\n\n@keyframes rotate {\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes stretch {\n  0% {\n    stroke-dasharray: 1, 200;\n    stroke-dashoffset: 0;\n  }\n  50% {\n    stroke-dasharray: 90, 200;\n    stroke-dashoffset: -35px;\n  }\n  100% {\n    stroke-dashoffset: -124px;\n  }\n}\n"
  },
  {
    "path": "components/spinner/spinner.tsx",
    "content": "import classes from './spinner.module.css'\n\nexport default function Spinner() {\n  return (\n    <>\n      <svg className={classes.ring} viewBox=\"25 25 50 50\" strokeWidth=\"5\">\n        <circle cx=\"50\" cy=\"50\" r=\"20\" />\n      </svg>\n    </>\n  )\n}\n"
  },
  {
    "path": "components/ui/accordion.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nconst Accordion = AccordionPrimitive.Root\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <AccordionPrimitive.Item\n    ref={ref}\n    className={cn(\"border-b-[2px]\", className)}\n    {...props}\n  />\n))\nAccordionItem.displayName = \"AccordionItem\"\n\nconst AccordionTrigger = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      ref={ref}\n      className={cn(\n        \"flex flex-1 items-center justify-between py-6 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n))\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName\n\nconst AccordionContent = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\",\n      className\n    )}\n    {...props}\n  >\n    <div className=\"pb-10 pt-0\">{children}</div>\n  </AccordionPrimitive.Content>\n))\nAccordionContent.displayName = AccordionPrimitive.Content.displayName\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n"
  },
  {
    "path": "components/ui/back-button.tsx",
    "content": "'use client'\n\nimport React from 'react'\nimport { Text } from '@/components/ui/text'\nimport { Button } from '@/components/ui/button'\nimport { useRouter } from 'next/navigation'\nimport { cn } from '@/utils/button-utils'\nimport { ChevronLeft } from 'lucide-react'\n\ntype BackButtonProps = {\n  text?: string\n  onClick?: () => void\n  className?: string\n}\n\nexport default function BackButton({\n  text = 'Go back',\n  onClick,\n  className,\n}: BackButtonProps) {\n  const router = useRouter()\n\n  const handleClick = () => {\n    if (onClick) {\n      onClick()\n    } else {\n      router.back()\n    }\n  }\n\n  return (\n    <Button\n      className={cn('group p-0', className)}\n      variant=\"noHoverGhost\"\n      onClick={handleClick}\n    >\n      <ChevronLeft className=\"mr-2 h-5 w-5 text-purple group-hover:text-purple/70\" />\n      <Text\n        className=\"text-purple group-hover:text-purple/70\"\n        variant=\"bodyMedium\"\n        semibold\n      >\n        {text}\n      </Text>\n    </Button>\n  )\n}\n"
  },
  {
    "path": "components/ui/badge.tsx",
    "content": "import * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@/utils/button-utils'\nconst badgeVariants = cva(\n  'w-fit inline-flex items-center rounded-md ring-1 ring-inset px-2 py-1 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 shadow-sm',\n  {\n    variants: {\n      variant: {\n        default: 'text-purple bg-indigo-500/10 ring-indigo-500/20',\n        success: 'bg-[#27ae60]/10 text-[#27AE60] ring-[#27ae60]/20',\n        destructive: 'bg-[#F06565]/10 text-[#F06565] ring-[#ff7373]/20',\n        help: 'bg-[#FFB154]/10 text-[#FFB154] ring-[#FFB154]/20',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "components/ui/button.tsx",
    "content": "import { cn } from '@/utils/button-utils'\nimport Spinner from '@/components/spinner/spinner'\nimport { VariantProps, cva } from 'class-variance-authority'\nimport { ButtonHTMLAttributes } from 'react'\nimport { forwardRef } from 'react'\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center rounded-xl text-sm font-medium ring-offset-background transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 focus:z-10',\n\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-gradient-to-br from-[#898AEB]/80 via-[#898dd9]/80 to-[#8e8ece]/80 text-white h-10 px-5 py-3  font-medium',\n        secondary: 'text-primary dark:text-dark h-10 px-4 py-2  font-medium',\n        outline:\n          'text-primary border border-border/80 dark:text-[#c2c3c9] h-10 px-4 py-2  bg-formDark font-normal',\n        ghost:\n          'text-primary dark:text-dark hover:bg-[#f5f7fa] dark:hover:bg-formDark h-10 px-4 py-2 rounded-none font-medium',\n        noHoverGhost:\n          'text-primary dark:text-dark h-10 px-4 py-2 rounded-none font-medium',\n        stylish:\n          ' bg-indigo-500/10 px-3 py-1 text-sm font-normal leading-6 text-purple border border-indigo-500/10', // ring-1 ring-inset ring-indigo-500/20\n        destructive:\n          ' px-3 py-1 text-sm font-medium leading-6 bg-red-500/10 text-red-500 ring-1 ring-red-500/10',\n        icon: 'text-primary bg-[#E0E0EC] dark:bg-formDark border border-border dark:text-dark h-12 px-5 py-2.5 ',\n        activeIcon:\n          'text-white bg-gradient-to-br from-[#898AEB] via-[#898dd9]/90 to-[#7d75d0] h-12 px-5 py-2.5  font-medium border-0 border-border dark:text-[#F2F3F9] focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',\n        menuItem:\n          'justify-between px-1.5 hover:bg-indigo-400/10 hover:text-foreground/80 rounded-sm cursor-default',\n      },\n      size: {\n        'x-lg': 'h-13 py-[0.8rem] px-[1.1rem]',\n        lg: 'h-11 py-3 px-4 text-md',\n        default: 'h-10 py-2 px-4',\n        sm: 'h-9 px-3 py-2',\n        'x-sm': 'h-8 px-3 py-2',\n      },\n      // Only used for the link since we can only used disabled prop on a button\n      linkDisabled: {\n        true: 'opacity-50 pointer-events-none',\n        false: '',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n      linkDisabled: false,\n    },\n  }\n)\n\ninterface ButtonProps\n  extends ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  isLoading?: boolean\n}\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    { className, children, variant, isLoading, size, linkDisabled, ...props },\n    ref\n  ) => {\n    return (\n      <button\n        className={cn(\n          buttonVariants({ variant, size, linkDisabled }),\n          className\n        )}\n        ref={ref}\n        disabled={isLoading}\n        {...props}\n      >\n        {isLoading ? (\n          <div className=\"flex items-center justify-center\">\n            <Spinner />\n          </div>\n        ) : (\n          children\n        )}\n      </button>\n    )\n  }\n)\n\nButton.displayName = 'Button'\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "components/ui/circular-slider.tsx",
    "content": "'use client'\n\nimport CircularSlider, {\n  CircularSliderProps,\n} from '@fseehawer/react-circular-slider'\nimport { useBackgroundOptions } from '@/store/use-background-options'\n\nconst CircularSliderComp: React.FC<CircularSliderProps> = () => {\n  const { gradientAngle, setGradientAngle } = useBackgroundOptions()\n\n  return (\n    <CircularSlider\n      label=\" \"\n      width={100}\n      min={0}\n      initialValue={gradientAngle}\n      max={360}\n      dataIndex={gradientAngle}\n      appendToValue=\"°\"\n      labelColor=\"#898aeb\"\n      labelBottom={true}\n      knobColor=\"#898aeb\"\n      knobSize={20}\n      progressColorFrom=\"#8e8ece\"\n      progressColorTo=\"rgb(202, 194, 255)\"\n      progressSize={6}\n      trackColor=\"rgb(99 102 241 / 0.1)\"\n      trackSize={9}\n      valueFontSize=\"1rem\"\n      onChange={(value: number) => {\n        if (typeof window === 'undefined') return\n        document?.documentElement.style.setProperty(\n          '--gradient-angle',\n          `${value.toString()}deg`\n        )\n        setGradientAngle(value)\n      }}\n    >\n      °\n    </CircularSlider>\n  )\n}\n\nexport default CircularSliderComp\n"
  },
  {
    "path": "components/ui/context-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nconst ContextMenu = ContextMenuPrimitive.Root\n\nconst ContextMenuTrigger = ContextMenuPrimitive.Trigger\n\nconst ContextMenuGroup = ContextMenuPrimitive.Group\n\nconst ContextMenuPortal = ContextMenuPrimitive.Portal\n\nconst ContextMenuSub = ContextMenuPrimitive.Sub\n\nconst ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup\n\nconst ContextMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <ContextMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </ContextMenuPrimitive.SubTrigger>\n))\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName\n\nconst ContextMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName\n\nconst ContextMenuContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Portal>\n    <ContextMenuPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </ContextMenuPrimitive.Portal>\n))\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName\n\nconst ContextMenuItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <ContextMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.CheckboxItem>\n))\nContextMenuCheckboxItem.displayName =\n  ContextMenuPrimitive.CheckboxItem.displayName\n\nconst ContextMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <ContextMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.RadioItem>\n))\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName\n\nconst ContextMenuLabel = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold text-foreground\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName\n\nconst ContextMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n    {...props}\n  />\n))\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName\n\nconst ContextMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\nContextMenuShortcut.displayName = \"ContextMenuShortcut\"\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioItem,\n  ContextMenuLabel,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuGroup,\n  ContextMenuPortal,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuRadioGroup,\n}\n"
  },
  {
    "path": "components/ui/dialog.tsx",
    "content": "// @ts-nocheck (for no reason build fails with this file)\n'use client'\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } from 'lucide-react'\n\nimport { cn } from '@/utils/button-utils'\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = ({\n  className,\n  ...props\n}: DialogPrimitive.DialogPortalProps) => (\n  <DialogPrimitive.Portal className={cn(className)} {...props} />\n)\nDialogPortal.displayName = DialogPrimitive.Portal.displayName\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 dark:bg-black/80',\n      className\n    )}\n    {...props}\n  />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-[#121212] p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full',\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-1.5 text-center sm:text-left',\n      className\n    )}\n    {...props}\n  />\n)\nDialogHeader.displayName = 'DialogHeader'\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className\n    )}\n    {...props}\n  />\n)\nDialogFooter.displayName = 'DialogFooter'\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      'text-lg font-semibold leading-none tracking-tight',\n      className\n    )}\n    {...props}\n  />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n}\n"
  },
  {
    "path": "components/ui/drawer.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport { Drawer as DrawerPrimitive } from 'vaul'\n\nimport { cn } from '@/utils/button-utils'\n\nconst Drawer = ({\n  shouldScaleBackground = true,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (\n  <DrawerPrimitive.Root\n    shouldScaleBackground={shouldScaleBackground}\n    {...props}\n  />\n)\nDrawer.displayName = 'Drawer'\n\nconst DrawerTrigger = DrawerPrimitive.Trigger\n\nconst DrawerPortal = DrawerPrimitive.Portal\n\nconst DrawerClose = DrawerPrimitive.Close\n\nconst DrawerOverlay = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Overlay\n    ref={ref}\n    className={cn('fixed inset-0 z-50 bg-black/80', className)}\n    {...props}\n  />\n))\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(\n        'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-[#121212]',\n        className\n      )}\n      {...props}\n    >\n      <div className=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n      {children}\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n))\nDrawerContent.displayName = 'DrawerContent'\n\nconst DrawerHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)}\n    {...props}\n  />\n)\nDrawerHeader.displayName = 'DrawerHeader'\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn('mt-auto flex flex-col gap-2 p-4', className)}\n    {...props}\n  />\n)\nDrawerFooter.displayName = 'DrawerFooter'\n\nconst DrawerTitle = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Title\n    ref={ref}\n    className={cn(\n      'text-lg font-semibold leading-none tracking-tight',\n      className\n    )}\n    {...props}\n  />\n))\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName\n\nconst DrawerDescription = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n))\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n}\n"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimport { Check, ChevronRight, Circle } from 'lucide-react'\n\nimport { cn } from '@/utils/button-utils'\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1.5 text-popover-foreground shadow-custom data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:shadow-sm',\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 min-w-[8rem] overflow-hidden rounded-md  border bg-popover p-1.5 text-popover-foreground shadow-custom data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:shadow-md',\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      'px-2 py-1.5 text-sm font-semibold',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn('ml-auto text-xs tracking-widest opacity-60', className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = 'DropdownMenuShortcut'\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "components/ui/gradient-text.tsx",
    "content": "import { cn } from '@/utils/button-utils'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport { type HTMLAttributes, forwardRef } from 'react'\n\nconst gradientTextVariants = cva('font-medium whitespace-normal font-sans', {\n  variants: {\n    variant: {\n      purple:\n        'bg-gradient-to-br from-[#898AEB] via-[#898dd9]/80 to-[#8e8ece] bg-clip-text text-transparent',\n    },\n  },\n  defaultVariants: {\n    variant: 'purple',\n  },\n})\n\ntype TextElement = HTMLParagraphElement | HTMLHeadingElement | HTMLSpanElement\n\nexport interface TextProps\n  extends HTMLAttributes<TextElement>,\n    VariantProps<typeof gradientTextVariants> {\n  as?: React.ElementType;\n}\n\nconst GradientText = forwardRef<TextElement, TextProps>(\n  ({ as = 'span', className, children, variant, ...props }, ref) => {\n    const Component = as\n    return (\n      <Component\n        ref={ref}\n        className={cn(gradientTextVariants({ variant }), className)}\n        {...props}\n      >\n        {children}\n      </Component>\n    )\n  }\n)\n\nGradientText.displayName = 'GradientText'\n\nexport { GradientText, gradientTextVariants }\n"
  },
  {
    "path": "components/ui/input.tsx",
    "content": "import * as React from 'react'\n\nimport { cn } from '@/utils/button-utils'\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nInput.displayName = 'Input'\n\nexport { Input }\n"
  },
  {
    "path": "components/ui/popover.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\n\nimport { cn } from '@/utils/button-utils'\n\nconst Popover = PopoverPrimitive.Root\n\nconst PopoverTrigger = PopoverPrimitive.Trigger\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 mb-2 mt-2 w-72 rounded-md border bg-popover p-4\ttext-popover-foreground shadow-xl outline-none backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-[#1a1c1f]/90',\n        className\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n))\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\nexport { Popover, PopoverTrigger, PopoverContent }\n"
  },
  {
    "path": "components/ui/scroll-area.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n))\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 border-t border-t-transparent p-[1px]\",\n      className\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n))\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName\n\nexport { ScrollArea, ScrollBar }\n"
  },
  {
    "path": "components/ui/select.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport * as SelectPrimitive from '@radix-ui/react-select'\nimport { Check, ChevronDown } from 'lucide-react'\n\nimport { cn } from '@/utils/button-utils'\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'flex h-10 w-full items-center justify-between rounded-md border border-border bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[#1a1c1f]/90',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = 'popper', ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-[#1a1c1f]/90',\n        position === 'popper' &&\n          'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectPrimitive.Viewport\n        className={cn(\n          'p-1',\n          position === 'popper' &&\n            'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n}\n"
  },
  {
    "path": "components/ui/separator.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport * as SeparatorPrimitive from '@radix-ui/react-separator'\n\nimport { cn } from '@/utils/button-utils'\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = 'horizontal', decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        'shrink-0 bg-border',\n        orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',\n        className\n      )}\n      {...props}\n    />\n  )\n)\nSeparator.displayName = SeparatorPrimitive.Root.displayName\n\nexport { Separator }\n"
  },
  {
    "path": "components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/utils/button-utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "components/ui/slider.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\n\nimport { cn } from '@/utils/button-utils'\nimport { MinusIcon, Plus } from 'lucide-react'\nimport { Button } from './button'\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {\n    onIncrement: () => void\n    onDecrement: () => void\n  }\n>(({ className, onIncrement, onDecrement, ...props }, ref) => (\n  <div className=\"flex w-full items-center gap-2.5\">\n    <Button\n      variant=\"ghost\"\n      className=\"h-6 rounded-md border border-border/80 bg-[#898aeb]/5 p-0.5\"\n      onClick={onDecrement}\n    >\n      <MinusIcon className=\"h-[1.15rem] w-[1.15rem] text-white/50\" />\n    </Button>\n    <SliderPrimitive.Root\n      ref={ref}\n      className={cn(\n        'relative flex w-full touch-none select-none items-center  disabled:cursor-not-allowed disabled:opacity-50',\n        className\n      )}\n      {...props}\n    >\n      <SliderPrimitive.Track className=\"relative h-2 w-full grow overflow-hidden rounded-[3px] bg-[#898aeb]/5 disabled:opacity-50\">\n        <SliderPrimitive.Range className=\"dark:bg-[#898aeb absolute h-full bg-[#898aeb]\" />\n      </SliderPrimitive.Track>\n      <SliderPrimitive.Thumb\n        aria-label=\"slider thumb\"\n        className=\"block h-5 w-5 rounded-md border border-border bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:outline-none active:ring-2 active:ring-ring active:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[#898aeb]\"\n      />\n    </SliderPrimitive.Root>\n    <Button\n      variant=\"ghost\"\n      className=\"h-6 rounded-md border border-border/80 bg-[#898aeb]/5 p-0.5\"\n      onClick={onIncrement}\n    >\n      <Plus className=\"h-[1.15rem] w-[1.15rem] text-white/50\" />\n    </Button>\n  </div>\n))\nSlider.displayName = SliderPrimitive.Root.displayName\n\nexport { Slider }\n"
  },
  {
    "path": "components/ui/spotlight-button.tsx",
    "content": "'use client'\n\nimport { cn } from '@/utils/button-utils'\nimport { Sparkles, Wand, Wand2 } from 'lucide-react'\n\ntype Props = {\n  text: string\n  onClick?: () => void\n  className?: string\n  disabled?: boolean\n  as?: 'button' | 'div'\n}\n\nexport default function SpotlightButton({\n  text,\n  onClick,\n  className,\n  disabled,\n  as = 'button',\n}: Props) {\n  const As = as\n\n  return (\n    <As\n      disabled={disabled}\n      onClick={onClick}\n      className={cn(\n        `group relative mx-auto inline-flex h-10 items-center overflow-hidden rounded-xl bg-[#191919] px-5 transition hover:bg-[#333] disabled:cursor-not-allowed disabled:opacity-50 ${\n          as === 'div' ? 'cursor-not-allowed opacity-50' : ''\n        }`,\n        className\n      )}\n      style={{\n        transition: 'background 0.8s cubic-bezier(0.6, 0.6, 0, 1)',\n      }}\n    >\n      <div className=\"absolute inset-0 flex items-center [container-type:inline-size]\">\n        <div className=\"absolute h-[100cqw] w-[100cqw] animate-spin bg-[conic-gradient(from_0_at_50%_50%,rgba(255,255,255,0.5)_0deg,transparent_60deg,transparent_300deg,rgba(255,255,255,0.5)_360deg)] opacity-100 transition duration-300  [animation-duration:3s]\"></div>\n      </div>\n\n      <div className=\"absolute inset-0.5 rounded-xl bg-[#121212]\"></div>\n\n      <div className=\"absolute bottom-0 left-1/2  h-2/3 w-4/5 -translate-x-1/2 rounded-xl bg-white/10 opacity-100 blur-md transition-all duration-500\"></div>\n\n      <span className=\"flex-center relative gap-2 bg-gradient-to-b from-white/25  to-white bg-clip-text text-[0.95rem] font-semibold text-transparent transition-all duration-200\">\n        {text}\n        {/* <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"20\"\n          height=\"20\"\n          viewBox=\"0 0 20 20\"\n          fill=\"#898aeb\"\n          className=\"fill-yellow-400/50 stroke-[1] text-yellow-400\"\n        >\n          <path\n            fillRule=\"evenodd\"\n            clip-rule=\"evenodd\"\n            d=\"M8.78716 1.30631C9.54417 0.897897 10.4558 0.897897 11.2128 1.30631C11.6773 1.5569 11.9959 2.00745 12.2705 2.4931C12.5496 2.98655 12.8557 3.65477 13.2362 4.48551L13.2515 4.51897C13.465 4.98521 13.5142 5.07453 13.5695 5.1376C13.6432 5.22168 13.7348 5.28823 13.8376 5.33241C13.9146 5.36555 14.0148 5.38467 14.5241 5.44374L14.6758 5.46133C15.5346 5.56092 16.2255 5.64104 16.7528 5.74591C17.2714 5.84907 17.7724 5.99934 18.1446 6.33101C18.8333 6.94469 19.1378 7.882 18.9413 8.78335C18.8351 9.2705 18.5181 9.6866 18.1591 10.075C17.7942 10.4698 17.2823 10.9408 16.646 11.5263L16.6204 11.5498C16.2256 11.9131 16.1522 11.9916 16.1087 12.0662C16.0431 12.179 16.008 12.3069 16.0067 12.4373C16.0059 12.5237 16.0289 12.6287 16.1826 13.1427L16.2194 13.2658C16.5219 14.278 16.7644 15.0897 16.8912 15.725C17.0154 16.348 17.0691 16.9748 16.814 17.5198C16.4996 18.1914 15.9089 18.6932 15.1954 18.895C14.6165 19.0587 14.0068 18.9043 13.4121 18.6809C12.8058 18.4531 12.0442 18.0824 11.0944 17.6202L10.7973 17.4756C10.3002 17.2336 10.1992 17.1943 10.108 17.1819C10.0388 17.1725 9.96857 17.1727 9.89942 17.1827C9.80827 17.1959 9.70762 17.236 9.21251 17.4819L8.95 17.6122C7.99018 18.0889 7.22084 18.471 6.60835 18.7063C6.00811 18.937 5.39184 19.0974 4.80608 18.9322C4.10046 18.7331 3.51435 18.2404 3.19686 17.5794C2.9333 17.0307 2.9853 16.3959 3.10914 15.7648C3.23551 15.1208 3.47943 14.2971 3.78375 13.2693L3.82132 13.1425C3.97348 12.6286 3.99619 12.5237 3.99518 12.4374C3.99367 12.3074 3.9585 12.1801 3.89312 12.0679C3.84967 11.9933 3.77636 11.9149 3.38201 11.5521L3.35636 11.5285C2.71907 10.9421 2.2064 10.4703 1.84096 10.0749C1.48148 9.68584 1.16412 9.26901 1.0582 8.78097C0.862923 7.88114 1.16677 6.94581 1.85355 6.33263C2.22605 6.00006 2.72776 5.84943 3.24719 5.74604C3.77524 5.64095 4.46722 5.56071 5.32742 5.46096L5.47593 5.44374C5.98525 5.38467 6.08536 5.36555 6.16243 5.33241C6.26517 5.28823 6.35675 5.22168 6.4305 5.1376C6.48583 5.07453 6.53495 4.98521 6.7485 4.51897L6.76382 4.48553C7.14432 3.65478 7.45037 2.98656 7.72945 2.4931C8.00411 2.00745 8.32267 1.5569 8.78716 1.30631ZM10.4994 2.62924C10.1877 2.46107 9.8123 2.46107 9.50059 2.62924C9.43332 2.66553 9.28926 2.78798 9.03749 3.23316C8.79387 3.66393 8.51414 4.27306 8.11476 5.14501C8.10351 5.16958 8.09238 5.19396 8.08134 5.21814C7.92089 5.56951 7.77976 5.87857 7.56016 6.12892C7.33891 6.38115 7.06416 6.58081 6.75595 6.71333C6.45005 6.84486 6.11257 6.8836 5.72888 6.92765C5.70248 6.93068 5.67585 6.93374 5.64901 6.93685L5.53511 6.95006C4.63228 7.05476 4.00141 7.12852 3.54048 7.22026C3.06387 7.31512 2.90781 7.40622 2.85431 7.45399C2.57151 7.70647 2.4464 8.09161 2.52681 8.46213C2.54202 8.53222 2.61474 8.69767 2.94458 9.05462C3.26357 9.39982 3.7306 9.83038 4.39948 10.4459C4.42037 10.4651 4.44111 10.4841 4.46167 10.503C4.75831 10.7753 5.01878 11.0144 5.19168 11.3113C5.38784 11.6481 5.49334 12.0301 5.49789 12.4198C5.5019 12.7634 5.4011 13.1023 5.2863 13.4883C5.27835 13.5151 5.27032 13.5421 5.26226 13.5693L5.23645 13.6565C4.91768 14.733 4.69485 15.4885 4.58381 16.0543C4.46863 16.6413 4.51399 16.8505 4.55145 16.9285C4.68218 17.2006 4.92352 17.4035 5.21407 17.4855C5.29731 17.509 5.51118 17.5177 6.06947 17.3032C6.60765 17.0964 7.31331 16.7469 8.31875 16.2475L8.54417 16.1356C8.57009 16.1227 8.59577 16.1099 8.62125 16.0972C8.99467 15.9112 9.32421 15.747 9.68492 15.695C9.89235 15.6651 10.1029 15.6642 10.3106 15.6925C10.6717 15.7416 11.0025 15.9032 11.3774 16.0862C11.403 16.0987 11.4288 16.1113 11.4548 16.124L11.7151 16.2506C12.7102 16.735 13.4083 17.0738 13.9406 17.2738C14.4923 17.4811 14.7039 17.472 14.7865 17.4486C15.0803 17.3655 15.3235 17.1589 15.453 16.8823C15.4894 16.8045 15.5327 16.5972 15.4174 16.019C15.3062 15.4613 15.0847 14.7175 14.7677 13.6569L14.7427 13.5732C14.7346 13.546 14.7265 13.519 14.7184 13.4922C14.6025 13.1061 14.5008 12.7671 14.504 12.4233C14.5077 12.0319 14.6131 11.6482 14.81 11.3099C14.983 11.0128 15.2437 10.7734 15.5407 10.5009C15.5613 10.482 15.582 10.4629 15.6029 10.4436C16.2708 9.82913 16.7371 9.39924 17.0556 9.05461C17.385 8.69824 17.4577 8.53305 17.473 8.4631C17.5539 8.09196 17.4285 7.70601 17.1449 7.45332C17.0915 7.40569 16.9356 7.31482 16.4597 7.22016C15.9994 7.12861 15.3696 7.05497 14.4682 6.95044L14.351 6.93685C14.3241 6.93374 14.2975 6.93068 14.2711 6.92765C13.8874 6.8836 13.55 6.84486 13.244 6.71333C12.9358 6.58081 12.6611 6.38115 12.4398 6.12892C12.2202 5.87857 12.0791 5.56951 11.9187 5.21814C11.9076 5.19396 11.8965 5.16958 11.8852 5.14501C11.4859 4.27306 11.2061 3.66393 10.9625 3.23316C10.7107 2.78798 10.5667 2.66553 10.4994 2.62924Z\"\n            fill=\"#898AEB\"\n            className=\"fill-yellow-400/50 stroke-[1] text-yellow-400\"\n          />\n        </svg> */}\n        <Wand2\n          style={{\n            transition: 'all 0.8s cubic-bezier(0.6, 0.6, 0, 1)',\n          }}\n          className=\"h-5 w-5 fill-[#898aeb]/20 stroke-[1] text-purple group-hover:rotate-180 \"\n        />\n      </span>\n    </As>\n  )\n}\n"
  },
  {
    "path": "components/ui/style/checkbox.module.css",
    "content": ".container {\n  display: inline-block;\n  vertical-align: middle;\n  margin-right: 10px;\n  cursor: pointer;\n  position: relative;\n}\n\n.container input {\n  display: none;\n}\n\n.container svg {\n  overflow: visible;\n}\n\n.path {\n  fill: none;\n  stroke: #8e8ece;\n  stroke-width: 5;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  -webkit-transition: stroke-dasharray 0.5s ease, stroke-dashoffset 0.5s ease;\n  transition: stroke-dasharray 0.5s ease, stroke-dashoffset 0.5s ease;\n  stroke-dasharray: 241 9999999;\n  stroke-dashoffset: 0;\n}\n\n.container input:checked ~ svg .path {\n  stroke-dasharray: 70.5096664428711 9999999;\n  stroke-dashoffset: -262.2723388671875;\n}"
  },
  {
    "path": "components/ui/switch.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-indigo-500/20 data-[state=unchecked]:bg-input',\n      className\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        'data-[state=checked]:bg-[#5f60a3] pointer-events-none block h-5 w-5 rounded-full bg-neutral-500 shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'\n      )}\n    />\n  </SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }\n"
  },
  {
    "path": "components/ui/tabs.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      'inline-flex w-full h-11 items-center justify-center rounded-md border border-border/20 bg-[#191919] p-1 text-[#949497]',\n      className\n    )}\n    {...props}\n  />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center w-full justify-center whitespace-nowrap rounded-sm px-6 py-2 text-[0.85rem] font-medium ring-offset-sidebar transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-[#141414] data-[state=active]:text-[#cfcfcf] data-[state=active]:shadow-sm\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
  },
  {
    "path": "components/ui/text-area.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "components/ui/text.tsx",
    "content": "import { cn } from '@/utils/button-utils'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport { type HTMLAttributes, forwardRef } from 'react'\n\nconst textVariants = cva('font-medium whitespace-normal font-sans', {\n  variants: {\n    variant: {\n      h1: 'text-[4rem] leading-[1.1] font-bold tracking-tight',\n      h2: 'text-[2.5rem] leading-[1.4] font-bold tracking-tight',\n      h3: 'text-[2rem] leading-tight font-bold tracking-[-0.02em]',\n      h4: 'text-2xl leading-normal font-bold tracking-[-0.015em]',\n      bodyXLarge: 'text-xl leading-[1.4] tracking-[-0.00625em]',\n      bodyLarge: 'text-lg leading-[1.45] tracking-[-0.00563em]',\n      bodyMedium: 'text-base leading-normal',\n      bodySmall: 'text-sm leading-[1.4]',\n      bodyXSmall: 'text-xs leading-[1.35]',\n    },\n  },\n  defaultVariants: {\n    variant: 'bodyXLarge',\n  },\n})\n\ntype TextElement = HTMLHeadingElement | HTMLParagraphElement\n\nexport interface TextProps\n  extends HTMLAttributes<TextElement>,\n    VariantProps<typeof textVariants> {\n  bold?: boolean\n  medium?: boolean\n  semibold?: boolean\n}\n\nconst Text = forwardRef<TextElement, TextProps>(\n  ({ className, children, variant, medium, bold, semibold, ...props }, ref) => {\n    const fontWeightClass = bold\n      ? 'font-bold'\n      : semibold\n      ? 'font-semibold'\n      : medium\n      ? 'font-medium'\n      : 'font-normal'\n\n    switch (variant) {\n      case 'h1':\n        return (\n          <h1\n            className={cn(\n              textVariants({ variant }),\n              fontWeightClass,\n              className\n            )}\n            ref={ref as React.Ref<HTMLHeadingElement>}\n            {...props}\n          >\n            {children}\n          </h1>\n        )\n      case 'h2':\n        return (\n          <h2\n            className={cn(\n              textVariants({ variant }),\n              fontWeightClass,\n              className\n            )}\n            ref={ref as React.Ref<HTMLHeadingElement>}\n            {...props}\n          >\n            {children}\n          </h2>\n        )\n      case 'h3':\n        return (\n          <h3\n            className={cn(\n              textVariants({ variant }),\n              fontWeightClass,\n              className\n            )}\n            ref={ref as React.Ref<HTMLHeadingElement>}\n            {...props}\n          >\n            {children}\n          </h3>\n        )\n      case 'h4':\n        return (\n          <h4\n            className={cn(\n              textVariants({ variant }),\n              fontWeightClass,\n              className\n            )}\n            ref={ref as React.Ref<HTMLHeadingElement>}\n            {...props}\n          >\n            {children}\n          </h4>\n        )\n      default:\n        return (\n          <p\n            className={cn(\n              textVariants({ variant }),\n              fontWeightClass,\n              className\n            )}\n            ref={ref as React.Ref<HTMLParagraphElement>}\n            {...props}\n          >\n            {children}\n          </p>\n        )\n    }\n  }\n)\n\nText.displayName = 'Text'\n\nexport { Text, textVariants }\n"
  },
  {
    "path": "components/ui/theme-button.tsx",
    "content": "'use client'\n\nimport * as React from 'react'\nimport { MoonStar, SunMoon } from 'lucide-react'\nimport { useTheme } from 'next-themes'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\n\nexport default function ModeToggle() {\n  const { setTheme } = useTheme()\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant={'ghost'} className=\"px-3 py-2\">\n          <SunMoon className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100  text-purple transition-all dark:-rotate-90 dark:scale-0\" />\n          <MoonStar className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 text-purple transition-all dark:rotate-0 dark:scale-100\" />\n          <span className=\"sr-only\">Toggle theme</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem\n          className=\"font-medium\"\n          onClick={() => setTheme('light')}\n        >\n          Light\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          className=\"font-medium\"\n          onClick={() => setTheme('dark')}\n        >\n          Dark\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          className=\"font-medium\"\n          onClick={() => setTheme('system')}\n        >\n          System\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n"
  },
  {
    "path": "components/ui/toast.tsx",
    "content": "import * as React from 'react'\nimport * as ToastPrimitives from '@radix-ui/react-toast'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { X } from 'lucide-react'\n\nimport { cn } from '@/utils/button-utils'\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',\n      className\n    )}\n    {...props}\n  />\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n  'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',\n  {\n    variants: {\n      variant: {\n        default: 'border bg-background',\n        destructive:\n          'destructive group border-destructive bg-destructive text-destructive-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  }\n)\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  )\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',\n      className\n    )}\n    {...props}\n  />\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',\n      className\n    )}\n    {...props}\n  >\n    <X className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn('text-sm font-medium', className)}\n    {...props}\n  />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn('text-sm opacity-90', className)}\n    {...props}\n  />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n}\n"
  },
  {
    "path": "components/ui/toaster.tsx",
    "content": "'use client'\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from '@/components/ui/toast'\nimport { useToast } from '@/hooks/use-toast'\n\nexport function Toaster() {\n  const { toasts } = useToast()\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        )\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  )\n}\n"
  },
  {
    "path": "components/ui/tooltip.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
  },
  {
    "path": "components/user-dropdown.tsx",
    "content": "// dropdown menu for user profile when logged in\n\n'use client'\n\nimport { Button } from '@/components/ui/button'\nimport SettingsDialog from './settings-dialog'\nimport ProfileDialog from './profile-dialog'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { toast } from '@/hooks/use-toast'\nimport { ChevronDown, LogOut, Settings, User, Zap } from 'lucide-react'\nimport { signOut } from 'next-auth/react'\nimport Image from 'next/image'\nimport React from 'react'\n\ninterface MenuItem {\n  icon: React.ComponentType<{ size?: number; className?: string }>\n  label: string\n  href?: string\n  isProfile?: boolean\n  isSettings?: boolean\n  separateFromHere?: boolean\n}\n\nconst baseItems: MenuItem[] = [\n  { icon: User, label: 'Profile', isProfile: true },\n  { icon: Settings, label: 'Settings', isSettings: true },\n  { href: '/upgrade', icon: Zap, label: 'Upgrade', separateFromHere: true },\n  { icon: LogOut, label: 'Logout' },\n]\n\nconst getMenuItems = (isCreator: boolean): MenuItem[] => {\n  return isCreator\n    ? [{ href: '/admin/write-article', icon: User, label: 'Admin' }, ...baseItems]\n    : baseItems\n}\n\nexport const UserDropDown = ({\n  username,\n  img,\n  isCreator,\n}: {\n  username: string\n  img: string\n  isCreator: boolean\n}) => {\n  const [profileOpen, setProfileOpen] = React.useState(false)\n  const [settingsOpen, setSettingsOpen] = React.useState(false)\n  const menuItems = getMenuItems(isCreator)\n  \n  const handleSignOut = async () => {\n    try {\n      await signOut()\n      if (typeof window !== 'undefined') localStorage.removeItem('downloaded')\n      toast({\n        title: 'Logging out..',\n      })\n    } catch (error) {\n      toast({\n        variant: 'destructive',\n        title: \"Couldn't sign you out!\",\n        description: 'Try again later.',\n      })\n    }\n  }\n\n  const handleMenuItemClick = (item: MenuItem) => {\n    if (item.isProfile) {\n      setProfileOpen(true)\n    } else if (item.isSettings) {\n      setSettingsOpen(true)\n    } else if (item.label === 'Logout') {\n      handleSignOut()\n    } else if (item.href) {\n      window.location.href = item.href\n    }\n  }\n\n  return (\n    <>\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild className=\"group flex items-center\">\n          <Button\n            variant=\"ghost\"\n            className=\"flex h-10 cursor-pointer items-center justify-center gap-x-2.5 rounded-xl bg-[#181818] px-4 py-2 font-medium text-dark\"\n          >\n            <div className=\"h-7 w-7 overflow-hidden\">\n              <Image\n                src={img}\n                alt={username}\n                unoptimized\n                width={32}\n                height={32}\n                className=\"h-full w-full rounded-full\"\n              />\n            </div>\n\n            <div className=\"flex translate-y-[-1px] items-center justify-center gap-1.5 truncate font-medium capitalize text-dark/80 md:text-base\">\n              <span className=\"sr-only\">Logged in as</span>\n              <span className=\"hidden text-sm md:inline\">\n                {username?.split(' ')[0]}\n              </span>\n\n              <ChevronDown\n                size={16}\n                className=\"translate-y-[1.6px] text-dark/80\"\n              />\n            </div>\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent\n          align=\"end\"\n          sideOffset={10}\n          className=\"w-[180px] rounded-xl border border-border/70 bg-[#151515]/95 p-1.5 py-2 shadow-xl backdrop-blur-lg\"\n        >\n          <DropdownMenuGroup>\n            {menuItems.map((item, index) => (\n              <React.Fragment key={item.label}>\n                <DropdownMenuItem\n                  className={`group cursor-pointer rounded-lg focus:bg-white ${\n                    index !== menuItems.length - 1 ? 'mb-1' : ''\n                  }`}\n                  onClick={() => handleMenuItemClick(item)}\n                >\n                  <div className=\"flex w-full items-center focus:shadow-md\">\n                    <item.icon\n                      size={18}\n                      className=\"mr-3 h-4 w-4 text-dark/80 group-focus:text-black/90\"\n                    />\n                    <span className=\"font-medium group-focus:text-black/90\">\n                      {item.label}\n                    </span>\n                  </div>\n                </DropdownMenuItem>\n                {item.separateFromHere && (\n                  <DropdownMenuSeparator\n                    key={`separator-${item.label}`}\n                    className=\"mb-1 opacity-80\"\n                  />\n                )}\n              </React.Fragment>\n            ))}\n          </DropdownMenuGroup>\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />\n      <ProfileDialog open={profileOpen} onOpenChange={setProfileOpen} />\n    </>\n  )\n}\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"styles/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/utils/buttonUtils\"\n  }\n}"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.8'\n\nservices:\n  postgres:\n    image: postgres:15-alpine\n    container_name: prismify-db\n    restart: unless-stopped\n    environment:\n      POSTGRES_DB: prismify\n      POSTGRES_USER: profile\n      POSTGRES_PASSWORD: password\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U profile -d prismify\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\nvolumes:\n  postgres_data:\n    driver: local "
  },
  {
    "path": "hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher.ts",
    "content": "import { useColorExtractor } from '@/store/use-color-extractor'\nimport { useImageQualityStore } from '@/store/use-image-quality'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport React, { useEffect } from 'react'\n\n/**\n * This hook encapsulates the logic used to automatically switch the aspect ratio of a screenshot\n * within a container. If the screenshot overflows the container, the aspect\n * ratio is adjusted to fit within the container.\n *\n * @param {Object} props - The properties for the hook.\n * @param {React.RefObject<HTMLDivElement>} props.containerRef - A ref to the container element.\n * @param {React.RefObject<HTMLDivElement>} props.screenshotRef - A ref to the screenshot element.\n */\n\nexport default function useAutomaticAspectRatioSwitcher({\n  containerRef,\n  screenshotRef,\n}: {\n  containerRef: React.RefObject<HTMLDivElement | null>\n  screenshotRef: React.RefObject<HTMLDivElement | null>\n}) {\n  const { resolution, setScrollScale } = useResizeCanvas()\n  const { quality } = useImageQualityStore()\n  const { imagesCheck } = useColorExtractor()\n\n  useEffect(() => {\n    const adjustAspectRatio = () => {\n      const parentEl = containerRef.current\n      const childEl = screenshotRef.current\n\n      if (!parentEl || !childEl) return\n\n      const isOverflowing =\n        parentEl.clientWidth <= childEl.clientWidth ||\n        parentEl.clientHeight <= childEl.clientHeight\n\n      if (isOverflowing) {\n        const isPortrait =\n          childEl.classList.contains('w-auto') &&\n          childEl.classList.contains('h-full')\n        const isLandscape =\n          childEl.classList.contains('w-full') &&\n          childEl.classList.contains('h-auto')\n\n        if (isPortrait) {\n          childEl.classList.remove('w-auto', 'h-full')\n          childEl.classList.add('w-full', 'h-auto')\n        } else if (isLandscape) {\n          childEl.classList.remove('w-full', 'h-auto')\n          childEl.classList.add('w-auto', 'h-full')\n        }\n      }\n    }\n\n    adjustAspectRatio()\n\n    const handleResize = () => {\n      // If the screen width is less than 768px then set the scrollScale to 1\n      if (window.innerWidth < 768) {\n        setScrollScale(1)\n      }\n      adjustAspectRatio()\n    }\n\n    window.addEventListener('resize', handleResize)\n\n    return () => {\n      window.removeEventListener('resize', handleResize)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [resolution, quality, imagesCheck, setScrollScale])\n}\n"
  },
  {
    "path": "hooks/canvas-area-hooks/use-resize-observer.ts",
    "content": "\nimport { useImageQualityStore } from '@/store/use-image-quality'\nimport { useResizeCanvas } from '@/store/use-resize-canvas'\nimport React, { useEffect } from 'react'\n\n/**\n * This hook encapsulates the logic for observing changes in the size of the screenshot element & automatically sets the DOM resolution and scale factor based on the size changes of a provided ref element.\n * \n * @param ref - The ref object that represents the HTMLDivElement to observe for size changes.\n */\n\nexport default function useCanvasResizeObserver(\n  ref: React.RefObject<HTMLDivElement | null>\n) {\n  const {\n    setScaleFactor,\n    resolution,\n    setExactDomResolution,\n    setDomResolution,\n  } = useResizeCanvas()\n  const { quality } = useImageQualityStore()\n  const [width, height]: number[] = resolution.split('x').map(Number)\n\n  useEffect(() => {\n    const element = ref.current\n\n    if (element) {\n      const resizeObserver = new ResizeObserver((entries) => {\n        for (let entry of entries) {\n          const { width: domWidth, height: domHeight } = entry.contentRect\n          const dynamicScaleFactor = width / domWidth\n          setScaleFactor(dynamicScaleFactor)\n          setExactDomResolution(`${domWidth}x${domHeight}`)\n          setDomResolution(\n            `${domWidth * dynamicScaleFactor * quality}x${\n              domHeight * dynamicScaleFactor * quality\n            }`\n          )\n        }\n      })\n\n      resizeObserver.observe(element)\n\n      return () => {\n        resizeObserver.unobserve(element)\n      }\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [resolution, quality])\n}\n"
  },
  {
    "path": "hooks/canvas-area-hooks/use-screen-size-warning-toast.ts",
    "content": "import React, { useEffect } from 'react'\nimport { toast } from '../use-toast'\n\n/**\n * This hook shows a warning toast if the screen size is less than 768px.\n */\n\nexport default function useScreenSizeWarningToast() {\n  useEffect(() => {\n    if (window.innerWidth <= 768 && !sessionStorage.getItem('toastShown')) {\n      toast({\n        title: 'Not optimized for mobile devices!',\n        description:\n          \"The editor is not optimized for mobile devices, hence many feature won't work as expected. Please use a desktop device for the best experience.\",\n      })\n      sessionStorage.setItem('toastShown', 'true')\n    }\n  }, [])\n}\n"
  },
  {
    "path": "hooks/use-editor.ts",
    "content": "import { useEditor } from '@tiptap/react'\nimport StarterKit from '@tiptap/starter-kit'\nimport { Color } from '@tiptap/extension-color'\nimport TextStyle from '@tiptap/extension-text-style'\n\nexport default function useTiptapEditor() {\n  const editor = useEditor({\n    extensions: [StarterKit, Color, TextStyle],\n    content: 'Double click to edit',\n  })\n\n  return { editor }\n}\n"
  },
  {
    "path": "hooks/use-event-listener.ts",
    "content": "import { RefObject, useEffect, useRef } from 'react'\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'\n\n// MediaQueryList Event based useEventListener interface\nfunction useEventListener<K extends keyof MediaQueryListEventMap>(\n  eventName: K,\n  handler: (event: MediaQueryListEventMap[K]) => void,\n  element: RefObject<MediaQueryList | null>,\n  options?: boolean | AddEventListenerOptions\n): void\n\n// Window Event based useEventListener interface\nfunction useEventListener<K extends keyof WindowEventMap>(\n  eventName: K,\n  handler: (event: WindowEventMap[K]) => void,\n  element?: undefined,\n  options?: boolean | AddEventListenerOptions\n): void\n\n// Element Event based useEventListener interface\nfunction useEventListener<\n  K extends keyof HTMLElementEventMap,\n  T extends HTMLElement = HTMLDivElement\n>(\n  eventName: K,\n  handler: (event: HTMLElementEventMap[K]) => void,\n  element: RefObject<T | null>,\n  options?: boolean | AddEventListenerOptions\n): void\n\n// Document Event based useEventListener interface\nfunction useEventListener<K extends keyof DocumentEventMap>(\n  eventName: K,\n  handler: (event: DocumentEventMap[K]) => void,\n  element: RefObject<Document | null>,\n  options?: boolean | AddEventListenerOptions\n): void\n\nfunction useEventListener<\n  KW extends keyof WindowEventMap,\n  KH extends keyof HTMLElementEventMap,\n  KM extends keyof MediaQueryListEventMap,\n  T extends HTMLElement | MediaQueryList | void = void\n>(\n  eventName: KW | KH | KM,\n  handler: (\n    event:\n      | WindowEventMap[KW]\n      | HTMLElementEventMap[KH]\n      | MediaQueryListEventMap[KM]\n      | Event\n  ) => void,\n  element?: RefObject<T | null>,\n  options?: boolean | AddEventListenerOptions\n) {\n  // Create a ref that stores handler\n  const savedHandler = useRef(handler)\n\n  useIsomorphicLayoutEffect(() => {\n    savedHandler.current = handler\n  }, [handler])\n\n  useEffect(() => {\n    // Define the listening target\n    const targetElement: T | Window = element?.current ?? window\n\n    if (!(targetElement && targetElement.addEventListener)) return\n\n    // Create event listener that calls handler function stored in ref\n    const listener: typeof handler = (event) => savedHandler.current(event)\n\n    targetElement.addEventListener(eventName, listener, options)\n\n    // Remove event listener on cleanup\n    return () => {\n      targetElement.removeEventListener(eventName, listener, options)\n    }\n  }, [eventName, element, options])\n}\n\nexport { useEventListener }\n"
  },
  {
    "path": "hooks/use-isomorphic-layout-effect.ts",
    "content": "import { useEffect, useLayoutEffect } from 'react'\n\nexport const useIsomorphicLayoutEffect =\n  typeof window !== 'undefined' ? useLayoutEffect : useEffect\n"
  },
  {
    "path": "hooks/use-media-query.ts",
    "content": "\nimport { useEffect, useState } from 'react'\n\nexport function useMediaQuery(query: string): boolean {\n  const getMatches = (query: string): boolean => {\n    // Prevents SSR issues\n    if (typeof window !== 'undefined') {\n      return window.matchMedia(query).matches\n    }\n    return false\n  }\n\n  const [matches, setMatches] = useState<boolean>(getMatches(query))\n\n  function handleChange() {\n    setMatches(getMatches(query))\n  }\n\n  useEffect(() => {\n    const matchMedia = window.matchMedia(query)\n\n    // Triggered at the first client-side load and if query changes\n    handleChange()\n\n    // Listen matchMedia\n    if (matchMedia.addListener) {\n      matchMedia.addListener(handleChange)\n    } else {\n      matchMedia.addEventListener('change', handleChange)\n    }\n\n    return () => {\n      if (matchMedia.removeListener) {\n        matchMedia.removeListener(handleChange)\n      } else {\n        matchMedia.removeEventListener('change', handleChange)\n      }\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [query])\n\n  return matches\n}"
  },
  {
    "path": "hooks/use-on-click-outside.ts",
    "content": "import { RefObject } from 'react'\n\nimport { useEventListener } from './use-event-listener'\n\ntype Handler = (event: MouseEvent) => void\n\nexport function useOnClickOutside<T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T | null>,\n  handler: Handler,\n  mouseEvent: 'mousedown' | 'mouseup' = 'mousedown'\n): void {\n  useEventListener(mouseEvent, (event) => {\n    const el = ref?.current\n\n    // Do nothing if clicking ref's element or descendent elements\n    if (!el || el.contains(event.target as Node)) {\n      return\n    }\n\n    handler(event)\n  })\n}\n"
  },
  {
    "path": "hooks/use-toast.ts",
    "content": "// Inspired by react-hot-toast library\nimport * as React from 'react'\n\nimport type { ToastActionElement, ToastProps } from '@/components/ui/toast'\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n  id: string\n  title?: React.ReactNode\n  description?: React.ReactNode\n  action?: ToastActionElement\n}\n\nconst actionTypes = {\n  ADD_TOAST: 'ADD_TOAST',\n  UPDATE_TOAST: 'UPDATE_TOAST',\n  DISMISS_TOAST: 'DISMISS_TOAST',\n  REMOVE_TOAST: 'REMOVE_TOAST',\n} as const\n\nlet count = 0\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_VALUE\n  return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n  | {\n      type: ActionType['ADD_TOAST']\n      toast: ToasterToast\n    }\n  | {\n      type: ActionType['UPDATE_TOAST']\n      toast: Partial<ToasterToast>\n    }\n  | {\n      type: ActionType['DISMISS_TOAST']\n      toastId?: ToasterToast['id']\n    }\n  | {\n      type: ActionType['REMOVE_TOAST']\n      toastId?: ToasterToast['id']\n    }\n\ninterface State {\n  toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId)\n    dispatch({\n      type: 'REMOVE_TOAST',\n      toastId: toastId,\n    })\n  }, TOAST_REMOVE_DELAY)\n\n  toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case 'ADD_TOAST':\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      }\n\n    case 'UPDATE_TOAST':\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      }\n\n    case 'DISMISS_TOAST': {\n      const { toastId } = action\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId)\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id)\n        })\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      }\n    }\n    case 'REMOVE_TOAST':\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        }\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      }\n  }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action)\n  listeners.forEach((listener) => {\n    listener(memoryState)\n  })\n}\n\ntype Toast = Omit<ToasterToast, 'id'>\n\nfunction toast({ ...props }: Toast) {\n  const id = genId()\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: 'UPDATE_TOAST',\n      toast: { ...props, id },\n    })\n  const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })\n\n  dispatch({\n    type: 'ADD_TOAST',\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss()\n      },\n    },\n  })\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  }\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState)\n\n  React.useEffect(() => {\n    listeners.push(setState)\n    return () => {\n      const index = listeners.indexOf(setState)\n      if (index > -1) {\n        listeners.splice(index, 1)\n      }\n    }\n  }, [state])\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),\n  }\n}\n\nexport { useToast, toast }\n"
  },
  {
    "path": "index.d.ts",
    "content": "/**\n * Represents a type that can be null or undefined.\n */\ntype Maybe = null | undefined\n\ndeclare module 'rgbaster' {\n  /**\n   * Analyzes the colors in an image.\n   * @param imageSrc - The source of the image to analyze.\n   * @param options - Optional settings for the analysis.\n   * @returns A promise that resolves to the analysis result.\n   */\n  export default function analyze(\n    imageSrc: string,\n    options?: { scale?: number; ignore?: string[] }\n  ): Promise<any>\n}\n\ndeclare module 'colorthief' {\n  /**\n   * Class for extracting colors from an image.\n   */\n  export default class ColorThief {\n    /**\n     * Gets the dominant color from an image.\n     * @param sourceImage - The source image. Can be an HTMLImageElement or a string (path to the image when run in Node).\n     * @param quality - Optional. Determines how many pixels are skipped before the next one is sampled. Defaults to 10.\n     * @returns A promise that resolves to an array of three integers representing red, green, and blue values.\n     */\n    getColor(\n      sourceImage: HTMLImageElement | string,\n      quality?: number\n    ): Promise<[number, number, number]>\n\n    /**\n     * Gets a color palette from an image.\n     * @param sourceImage - The source image. Can be an HTMLImageElement or a string (path to the image when run in Node).\n     * @param colorCount - Optional. Determines the size of the returned palette. Defaults to 10.\n     * @param quality - Optional. Determines how many pixels are skipped before the next one is sampled. Defaults to 10.\n     * @returns A promise that resolves to an array of colors, each color itself an array of three integers.\n     */\n    getPalette(\n      sourceImage: HTMLImageElement | string,\n      colorCount?: number,\n      quality?: number\n    ): Promise<Array<[number, number, number]>>\n  }\n}\n\ndeclare module 'sanitize-html'\n"
  },
  {
    "path": "libs/prismadb.ts",
    "content": "import { PrismaClient } from '@prisma/client'\n\ndeclare global {\n  var prisma: PrismaClient | undefined\n}\n\nconst prismadb = globalThis.prisma || new PrismaClient()\nif (process.env.NODE_ENV !== 'production') globalThis.prisma = prismadb\n\nexport default prismadb\n"
  },
  {
    "path": "libs/validators/article-post-validator.ts",
    "content": "import { z } from 'zod'\nimport sanitizeHtml from 'sanitize-html'\n\nexport const postSchema = z\n  .object({\n    title: z\n      .string()\n      .max(100, 'Title should be at most 100 characters')\n      .min(10, 'Title must be at least 10 characters'),\n    summary: z\n      .string()\n      .max(300, 'Summary should be at most 300 characters')\n      .min(10, 'Summary must be at least 10 characters'),\n    category: z\n      .string()\n      .max(200, 'Category must be at most 200 characters')\n      .optional(),\n    slug: z.string().max(200, 'Slug too long.').min(1, 'Please provide a slug'),\n    content: z.string(),\n    imageUrl: z.string().optional(),\n  })\n  .transform((data) => {\n    return {\n      ...data,\n      content: sanitizeHtml(data.content, {\n        allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),\n        allowedAttributes: {\n          img: ['src', 'alt', 'width', 'height'],\n        },\n      }),\n    }\n  })\n"
  },
  {
    "path": "libs/validators/user-settings-validator.ts",
    "content": "import { z } from 'zod'\n\nexport const userSettingsSchema = z.object({\n  name: z.string().min(1).max(50).optional().or(z.undefined()),\n  image: z.string().url().optional().or(z.literal('')).or(z.undefined()),\n})\n"
  },
  {
    "path": "license.md",
    "content": "1. You are free to use Prismify for any purpose, commercial or non-commercial. While not required, I would appreciate it if you could give proper attribution or credits to Prismify when using it in your projects, products, or services.\n\n2. You are not allowed to modify or alter the source code and redistribute it under the different name or branding. Any modifications made to the webapp should be for personal use only.\n\n**NOTE**: This license is subject to change. I reserve the right to change this license at any time without prior notice.\n"
  },
  {
    "path": "middleware.ts",
    "content": "import { withAuth } from 'next-auth/middleware'\n\nexport default withAuth(\n  function middleware(req) {\n    console.log('Welcome admin')\n  },\n  {\n    callbacks: {\n      authorized: ({ token }) => token?.isCreator === true,\n    },\n  }\n)\n\nexport const config = {\n  matcher: ['/admin/:path*'],\n}\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\n\nconst nextConfig = {\n  images: {\n    // domains: ['lh3.googleusercontent.com', 'avatars.githubusercontent.com', 'prismify.notion.site'],\n      remotePatterns: [\n        {\n          protocol: 'https',\n          hostname: 'lh3.googleusercontent.com',\n          port: '',\n          pathname: '/**',\n        },\n        {\n          protocol: 'https',\n          hostname: 'avatars.githubusercontent.com',\n          port: '',\n          pathname: '/**',\n        },\n        {\n          protocol: 'https',\n          hostname: 'prismify.notion.site',\n          port: '',\n          pathname: '/**',\n        },\n      ],\n  },\n  logging: {\n    fetches: {\n      fullUrl: true,\n    },\n  },\n  compiler: {\n    removeConsole: {\n      exclude: ['error'],\n    },\n  },\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"prismify\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"author\": {\n    \"name\": \"Silson\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+\"\n  },\n  \"displayName\": \"Prismify\",\n  \"description\": \"Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics for websites, social media, and more. With Prismify, you get browser frames, gradient backgrounds, text, annotations.\",\n  \"scripts\": {\n    \"dev\": \"next dev --turbopack\",\n    \"build\": \"prisma generate && next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"setup\": \"pnpm install && pnpm exec prisma generate\",\n    \"db:push\": \"prisma db push\",\n    \"db:studio\": \"prisma studio\",\n    \"docker:up\": \"docker-compose up -d\",\n    \"docker:down\": \"docker-compose down\",\n    \"docker:clean\": \"docker-compose down -v\"\n  },\n  \"dependencies\": {\n    \"@fseehawer/react-circular-slider\": \"^2.6.1\",\n    \"@imgly/background-removal\": \"^1.6.0\",\n    \"@next-auth/prisma-adapter\": \"^1.0.7\",\n    \"@prisma/client\": \"^5.7.1\",\n    \"@radix-ui/react-accordion\": \"^1.2.3\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-compose-refs\": \"^1.1.1\",\n    \"@radix-ui/react-context-menu\": \"^2.2.6\",\n    \"@radix-ui/react-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.6\",\n    \"@radix-ui/react-popover\": \"^1.1.6\",\n    \"@radix-ui/react-primitive\": \"^2.0.2\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.3\",\n    \"@radix-ui/react-select\": \"^2.1.6\",\n    \"@radix-ui/react-separator\": \"^1.1.2\",\n    \"@radix-ui/react-slider\": \"^1.2.3\",\n    \"@radix-ui/react-slot\": \"^1.1.2\",\n    \"@radix-ui/react-switch\": \"^1.1.3\",\n    \"@radix-ui/react-tabs\": \"^1.1.3\",\n    \"@radix-ui/react-toast\": \"^1.2.6\",\n    \"@radix-ui/react-tooltip\": \"^1.1.8\",\n    \"@tailwindcss/typography\": \"^0.5.10\",\n    \"@tanstack/react-query\": \"^4.32.6\",\n    \"@tiptap/extension-color\": \"^2.1.12\",\n    \"@tiptap/extension-image\": \"^2.1.13\",\n    \"@tiptap/extension-link\": \"^2.1.13\",\n    \"@tiptap/extension-placeholder\": \"^2.1.13\",\n    \"@tiptap/extension-text-style\": \"^2.1.12\",\n    \"@tiptap/extension-underline\": \"^2.1.13\",\n    \"@tiptap/react\": \"^2.1.11\",\n    \"@tiptap/starter-kit\": \"^2.1.11\",\n    \"@types/react\": \"19.0.0\",\n    \"@types/react-dom\": \"19.0.0\",\n    \"@vercel/analytics\": \"^1.0.2\",\n    \"autoprefixer\": \"10.4.14\",\n    \"axios\": \"^1.4.0\",\n    \"class-variance-authority\": \"^0.6.1\",\n    \"clsx\": \"^1.2.1\",\n    \"colorthief\": \"^2.4.0\",\n    \"eslint\": \"8.44.0\",\n    \"eslint-config-next\": \"15.2.4\",\n    \"file-saver\": \"^2.0.5\",\n    \"font-picker-react\": \"^3.5.2\",\n    \"html-to-image\": \"^1.11.11\",\n    \"just-throttle\": \"^4.2.0\",\n    \"lucide-react\": \"^0.265.0\",\n    \"next\": \"15.1.11\",\n    \"next-auth\": \"^4.24.11\",\n    \"next-themes\": \"^0.2.1\",\n    \"onnxruntime-web\": \"1.21.0-dev.20250206-d981b153d3\",\n    \"postcss\": \"8.4.25\",\n    \"react\": \"19.0.0\",\n    \"react-colorful\": \"^5.6.1\",\n    \"react-dom\": \"19.0.0\",\n    \"react-dropzone\": \"^14.2.3\",\n    \"react-hotkeys-hook\": \"^4.4.1\",\n    \"react-image-crop\": \"^10.1.8\",\n    \"react-joystick-component\": \"^6.2.1\",\n    \"react-moveable\": \"^0.54.1\",\n    \"react-selecto\": \"^1.26.0\",\n    \"react-wrap-balancer\": \"^1.0.0\",\n    \"rgbaster\": \"^2.1.1\",\n    \"sanitize-html\": \"^2.12.1\",\n    \"slugify\": \"^1.6.6\",\n    \"tailwind-merge\": \"^1.13.2\",\n    \"tailwindcss\": \"3.3.2\",\n    \"tailwindcss-animate\": \"^1.0.6\",\n    \"typescript\": \"5.1.6\",\n    \"vaul\": \"^0.8.0\",\n    \"zod\": \"^3.21.4\",\n    \"zundo\": \"2.0.0\",\n    \"zustand\": \"^4.3.9\"\n  },\n  \"devDependencies\": {\n    \"@types/file-saver\": \"^2.0.5\",\n    \"@types/node\": \"20.4.1\",\n    \"prettier\": \"~2.8.8\",\n    \"prettier-plugin-tailwindcss\": \"^0.3.0\",\n    \"prisma\": \"^5.7.1\"\n  },\n  \"packageManager\": \"pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589\"\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "prettier.config.js",
    "content": "module.exports = {\n  plugins: [require(\"prettier-plugin-tailwindcss\")],\n  singleQuote: true,\n  semi: false,\n  tabWidth: 2,\n};\n"
  },
  {
    "path": "prisma/schema.prisma",
    "content": "generator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider     = \"postgresql\"\n  url          = env(\"DATABASE_URL\")\n  relationMode = \"prisma\"\n}\n\nmodel User {\n  id             String    @id @default(cuid())\n  name           String?\n  email          String?   @unique\n  emailVerified  DateTime?\n  image          String?\n  hashedPassword String?\n  createdAt      DateTime  @default(now())\n  updatedAt      DateTime  @updatedAt\n  isCreator      Boolean   @default(false)\n\n  saves            Saves[]\n  accounts         Account[]\n  settings         UserSettings?\n}\n\nmodel Account {\n  id                String  @id @default(cuid())\n  userId            String\n  type              String\n  provider          String\n  providerAccountId String\n  refresh_token     String? @db.Text\n  access_token      String? @db.Text\n  expires_at        Int?\n  token_type        String?\n  scope             String?\n  id_token          String? @db.Text\n  session_state     String?\n\n  user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@unique([provider, providerAccountId])\n  @@index([userId])\n}\n\nmodel Article {\n  id       String  @id @default(cuid())\n  title    String\n  slug     String  @unique\n  summary  String? @db.Text\n  imageUrl String? @db.Text\n  content  String  @db.Text\n  category String?\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  @@index([slug])\n}\n\nmodel Saves {\n  id       String  @id @default(cuid())\n  imageUrl String\n  isPublic Boolean @default(true)\n  userId   String\n  user     User    @relation(fields: [userId], references: [id])\n\n  @@index([userId])\n}\n\nmodel UserSettings {\n  id       String  @id @default(cuid())\n  userId   String  @unique\n  isPublic Boolean @default(true)\n  user     User    @relation(fields: [userId], references: [id])\n}\n"
  },
  {
    "path": "providers/index.tsx",
    "content": "'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { Provider } from 'react-wrap-balancer'\nimport { SessionProvider } from 'next-auth/react'\nimport { ThemeProvider } from 'next-themes'\n\ntype ProviderProps = {\n  children: React.ReactNode\n}\n\nexport default function Providers({ children }: ProviderProps) {\n  const queryClient = new QueryClient({\n    defaultOptions: {\n      queries: {\n        refetchOnWindowFocus: false,\n        refetchOnMount: false,\n        retry: 2,\n      },\n    },\n  })\n\n  return (\n    <SessionProvider>\n      <QueryClientProvider client={queryClient}>\n        <ThemeProvider attribute=\"class\" defaultTheme='dark'>\n        <Provider>{children}</Provider>\n        </ThemeProvider>\n      </QueryClientProvider>\n    </SessionProvider>\n  )\n}\n"
  },
  {
    "path": "store/use-active-index.ts",
    "content": "import { create } from 'zustand'\n\ninterface ActiveIndexState {\n  activeIndex: number\n  setActiveIndex: (index: number) => void\n}\n\nexport const useActiveIndexStore = create<ActiveIndexState>((set) => ({\n  activeIndex: 0,\n  setActiveIndex: (index) => set({ activeIndex: index }),\n}))\n"
  },
  {
    "path": "store/use-background-options.ts",
    "content": "import { create } from 'zustand'\n\ninterface BackgroundOptionsState {\n  background: string\n  setBackground: (background: string) => void\n\n  isBackgroundClicked: boolean\n  setIsBackgroundClicked: (isBackgroundClicked: boolean) => void\n\n  backgroundType: 'mesh' | 'solid' | 'gradient' | 'pattern' | 'custom'\n  setBackgroundType: (\n    backgroundType: 'mesh' | 'solid' | 'gradient' | 'pattern' | 'custom'\n  ) => void\n\n  solidColor: string\n  setSolidColor: (solidColor: string) => void\n\n  imageBackground: string | null\n  setImageBackground: (imageBackground: string | null) => void\n\n  attribution: { name: string | null; link: string | null }\n  setAttribution: (attribution: {\n    name: string | null\n    link: string | null\n  }) => void\n\n  highResBackground: boolean\n  setHighResBackground: (highResBackground: boolean) => void\n\n  gradientAngle: number\n  setGradientAngle: (gradientAngle: number) => void\n\n  noise: number\n  setNoise: (noise: number) => void\n}\n\nexport const useBackgroundOptions = create<BackgroundOptionsState>()((set) => ({\n  background:\n    'linear-gradient(var(--gradient-angle), rgb(202, 194, 255), rgb(242, 231, 248) 100%)',\n  setBackground: (background) => set({ background }),\n\n  solidColor: '',\n  setSolidColor: (solidColor) => set({ solidColor }),\n\n  imageBackground: null,\n  setImageBackground: (imageBackground) => set({ imageBackground }),\n\n  attribution: { name: null, link: null },\n  setAttribution: (attribution) => set({ attribution }),\n\n  highResBackground: true,\n  setHighResBackground: (highResBackground) => set({ highResBackground }),\n\n  backgroundType: 'gradient',\n  setBackgroundType: (backgroundType) => set({ backgroundType }),\n\n  gradientAngle: 170,\n  setGradientAngle: (gradientAngle) => set({ gradientAngle }),\n\n  noise: 0,\n  setNoise: (noise) => set({ noise }),\n\n  isBackgroundClicked: false,\n  setIsBackgroundClicked: (isBackgroundClicked) => set({ isBackgroundClicked }),\n}))\n"
  },
  {
    "path": "store/use-color-extractor.ts",
    "content": "import { create } from 'zustand'\n\ninterface ActiveIndexState {\n  extractedColor: {}\n  setExtractedColor: (extractedColor: {}) => void\n\n  imagesCheck: string[]\n  setImagesCheck: (imagesCheck: string[]) => void\n}\n\nexport const useColorExtractor = create<ActiveIndexState>()((set) => ({\n  extractedColor: {},\n  setExtractedColor: (extractedColor) => set(() => ({ extractedColor })),\n\n  imagesCheck: [],\n  setImagesCheck: (imagesCheck) => set(() => ({ imagesCheck })),\n}))\n"
  },
  {
    "path": "store/use-frame-options.ts",
    "content": "import { create } from 'zustand'\n\nexport type FrameTypes =\n  | 'Arc'\n  | 'MacOS Dark'\n  | 'MacOS Light'\n  | 'Shadow'\n  | 'None'\n\ninterface FrameOptionsState {\n  frameHeight: 'small' | 'medium' | 'large' | string\n  setFrameHeight: (height: string) => void\n\n  showSearchBar: boolean\n  setShowSearchBar: (searchBar: boolean) => void\n\n  showStroke: boolean\n  setShowStroke: (showStroke: boolean) => void\n\n  macOsDarkColor: string\n  setMacOsDarkColor: (color: string) => void\n\n  macOsLightColor: string\n  setMacOsLightColor: (color: string) => void\n\n  arcDarkMode: boolean\n  setArcDarkMode: (darkMode: boolean) => void\n\n  hideButtons: boolean\n  setHideButtons: (hideButtons: boolean) => void\n\n  hasButtonColor: boolean\n  setHasButtonColor: (hasButtonColor: boolean) => void\n}\n\nexport const useFrameOptions = create<FrameOptionsState>()((set) => ({\n  frameHeight: 'small',\n  setFrameHeight: (frameHeight) => set({ frameHeight }),\n\n  showSearchBar: false,\n  setShowSearchBar: (searchBar) => set({ showSearchBar: searchBar }),\n\n  showStroke: true,\n  setShowStroke: (showStroke) => set({ showStroke }),\n\n  macOsDarkColor: '#1a1a1a',\n  setMacOsDarkColor: (color) => set({ macOsDarkColor: color }),\n\n  macOsLightColor: '#f4f4f4',\n  setMacOsLightColor: (color) => set({ macOsLightColor: color }),\n\n  arcDarkMode: false,\n  setArcDarkMode: (darkMode) => set({ arcDarkMode: darkMode }),\n\n  hideButtons: false,\n  setHideButtons: (hideButtons) => set({ hideButtons }),\n\n  hasButtonColor: true,\n  setHasButtonColor: (hasButtonColor) => set({ hasButtonColor }),\n}))\n"
  },
  {
    "path": "store/use-image-options.ts",
    "content": "import { create, useStore } from 'zustand'\nimport { temporal, TemporalState } from 'zundo'\nimport throttle from 'just-throttle'\nimport { FrameTypes } from './use-frame-options'\n\nexport interface ImageStyle {\n  imageSize: string\n  imageRoundness: number\n  imageShadow: string\n  shadowPreview: string\n  shadowName: string\n  shadowOpacity: number\n  shadowColor: string\n  borderSize: string | null\n  borderColor: string\n  insetSize: string\n  insetColor: string\n  rotate: string\n  rotateX: number\n  rotateY: number\n  rotateZ: number\n  perspective: number\n  translateX: number\n  translateY: number\n  zIndex: number\n  hasFrame?: boolean\n}\n\nexport interface ImageItem {\n  id: number\n  image: string\n  extractedColors?: { color: string; count: number }[]\n  linearGradients?: string[]\n  meshGradients?: string[]\n  radialGradients?: string[]\n  dominantColor?: string\n  pallettes?: string[]\n  style: ImageStyle\n  frame?: FrameTypes\n}\n\ninterface ImageOptionsState {\n  scale: number\n  setScale: (scale: number) => void\n\n  accordionOpen: {\n    appearanceOpen: boolean\n    shadowOpen: boolean\n    borderOpen: boolean\n  }\n  setAccordionOpen: (accordionOpen: {\n    appearanceOpen: boolean\n    shadowOpen: boolean\n    borderOpen: boolean\n  }) => void\n\n  initialImageUploaded: boolean\n  setInitialImageUploaded: (initialImageUploaded: boolean) => void\n\n  images: ImageItem[]\n  setImages: (images: ImageItem[]) => void\n  addImage: (image: ImageItem) => void\n  updateImageStyle: (id: number, style: Partial<ImageStyle>) => void\n  updateImage: (id: number, data: Partial<ImageItem>) => void\n  getImage: (id: number) => ImageItem | undefined\n  getImageIndex: (id: number) => number\n\n  texts: {\n    id: number\n    content: string\n    style: {\n      textSize: string\n      textColor: string\n      textAlign: 'left' | 'center' | 'right'\n      fontWeight: number\n      fontFamily: string\n      letterSpacing: number\n      textShadow: string\n      shadowName: string\n      shadowColor: string\n      shadowOpacity: number\n      hasBackground: boolean\n      backgroundColor: string\n      padding: string\n      zIndex: number\n      position: string\n    }\n  }[]\n  setTexts: (\n    texts: {\n      id: number\n      content: string\n      style: {\n        textSize: string\n        textColor: string\n        textAlign: 'left' | 'center' | 'right'\n        fontWeight: number\n        fontFamily: string\n        letterSpacing: number\n        textShadow: string\n        shadowName: string\n        shadowColor: string\n        shadowOpacity: number\n        hasBackground: boolean\n        backgroundColor: string\n        padding: string\n        zIndex: number\n        position: string\n      }\n    }[]\n  ) => void\n\n  defaultStyle: {\n    imageSize: string\n    imageRoundness: number\n    imageShadow: string\n    shadowPreview: string\n    shadowName: string\n    shadowOpacity: number\n    shadowColor: string\n    borderSize: string\n    borderColor: string\n    insetSize: string\n    insetColor: string\n    rotate: string\n    rotateX: number\n    rotateY: number\n    rotateZ: number\n    perspective: number\n    translateX: number\n    translateY: number\n    zIndex: number\n  }\n\n  defaultTextStyle: {\n    textSize: string\n    textColor: string\n    textAlign: 'left' | 'center' | 'right'\n    fontWeight: number\n    fontFamily: string\n    letterSpacing: number\n    textShadow: string\n    shadowName: string\n    shadowColor: string\n    shadowOpacity: number\n    hasBackground: boolean\n    backgroundColor: string\n    padding: string\n    zIndex: number\n    position: string\n  }\n}\n\nexport const useImageOptions = create(\n  temporal<ImageOptionsState>(\n    (set, get) => ({\n      scale: 1,\n      setScale: (scale) => set({ scale }),\n\n      accordionOpen: {\n        appearanceOpen: true,\n        shadowOpen: true,\n        borderOpen: false,\n      },\n      setAccordionOpen: (accordionOpen) => set({ accordionOpen }),\n\n      initialImageUploaded: false,\n      setInitialImageUploaded: (initialImageUploaded) =>\n        set({ initialImageUploaded }),\n\n      defaultStyle: {\n        imageSize: '0.8',\n        imageRoundness: 0.5,\n        imageShadow: '',\n        shadowPreview: '0 25px 50px -12px #000000',\n        shadowOpacity: 0.22,\n        shadowName: 'Medium',\n        shadowColor: '#000',\n        borderSize: '0',\n        borderColor: '#ffffff50',\n        insetSize: '0',\n        insetColor: '#fff',\n        rotate: '0',\n        rotateX: 0.0001,\n        rotateY: 0,\n        rotateZ: 0,\n        perspective: 2000,\n        translateX: 0,\n        translateY: 0,\n        hasFrame: false,\n        zIndex: 2,\n      },\n\n      defaultTextStyle: {\n        textSize: '3',\n        textColor: '#151515',\n        textAlign: 'center',\n        fontWeight: 500,\n        fontFamily: 'Inter',\n        letterSpacing: -0.03,\n        textShadow: '1px 1px 8px',\n        shadowName: 'Bottom',\n        shadowColor: '#000',\n        shadowOpacity: 0.16,\n        hasBackground: false,\n        backgroundColor: '#ffffff50',\n        padding: '0',\n        zIndex: 2,\n        position: '',\n      },\n\n      images: [],\n      setImages: (images) => set({ images }),\n      addImage: (image) =>\n        set((state) => ({\n          images: [...state.images, image],\n        })),\n      updateImageStyle: (id, style) =>\n        set((state) => ({\n          images: state.images.map((img) =>\n            img.id === id ? { ...img, style: { ...img.style, ...style } } : img\n          ),\n        })),\n      updateImage: (id, data) =>\n        set((state) => ({\n          images: state.images.map((img) =>\n            img.id === id\n              ? {\n                  ...img,\n                  ...data,\n                  style: { ...img.style, ...(data.style ?? {}) },\n                }\n              : img\n          ),\n        })),\n      getImage: (id) => get().images.find((img) => img.id === id),\n      getImageIndex: (id) => get().images.findIndex((img) => img.id === id),\n\n      texts: [],\n      setTexts: (texts) => set({ texts }),\n    }),\n    {\n      limit: 30,\n      handleSet: (handleSet) =>\n        throttle<typeof handleSet>((state) => {\n          handleSet(state)\n        }, 800),\n    }\n  )\n)\n\nexport const useTemporalStore = <T extends unknown>(\n  selector: (state: TemporalState<ImageOptionsState>) => T,\n  equality?: (a: T, b: T) => boolean\n) => useStore(useImageOptions.temporal, selector, equality)\n\ninterface SelectedLayerState {\n  selectedImage: number | null\n  setSelectedImage: (selectedImage: number | null) => void\n\n  selectedText: number | null\n  setSelectedText: (selectedText: number | null) => void\n\n  enableCrop: boolean\n  setEnableCrop: (enableCrop: boolean) => void\n}\n\nexport const useSelectedLayers = create<SelectedLayerState>((set) => ({\n  selectedImage: null,\n  setSelectedImage: (selectedImage) => set({ selectedImage }),\n\n  selectedText: null,\n  setSelectedText: (selectedText) => set({ selectedText }),\n\n  enableCrop: false,\n  setEnableCrop: (enableCrop) => set({ enableCrop }),\n}))\n"
  },
  {
    "path": "store/use-image-quality.ts",
    "content": "import { create } from 'zustand'\n\ninterface ImageQualityState {\n  quality: number\n  setQuality: (quality: number) => void\n\n  fileType: 'PNG' | 'JPG' | 'SVG' | 'WEBP'\n  setFileType: (fileType: 'PNG' | 'JPG' | 'SVG' | 'WEBP') => void\n}\n\nexport const useImageQualityStore = create<ImageQualityState>()((set) => ({\n  quality: 1,\n  setQuality: (quality) => set(() => ({ quality: quality })),\n\n  fileType: 'PNG',\n  setFileType: (fileType) => set(() => ({ fileType: fileType })),\n}))\n"
  },
  {
    "path": "store/use-moveable.ts",
    "content": "import { create } from 'zustand'\n\ninterface MoveableState {\n  showControls: boolean\n  setShowControls: (showControls: boolean) => void\n\n  showTextControls: boolean\n  setShowTextControls: (showTextControls: boolean) => void\n\n  isEditable: boolean\n  setIsEditable: (isEditable: boolean) => void\n\n  isSelecting: boolean\n  setIsSelecting: (isSelecting: boolean) => void\n\n  isMultipleTargetSelected: boolean\n  setIsMultipleTargetSelected: (isMultipleTargetSelected: boolean) => void\n}\n\nexport const useMoveable = create<MoveableState>()((set) => ({\n  showControls: false,\n  setShowControls: (showControls) => set({ showControls }),\n\n  showTextControls: false,\n  setShowTextControls: (showTextControls) => set({ showTextControls }),\n\n  isEditable: false,\n  setIsEditable: (isEditable) => set({ isEditable }),\n\n  isSelecting: false,\n  setIsSelecting: (isSelecting) => set({ isSelecting }),\n\n  isMultipleTargetSelected: false,\n  setIsMultipleTargetSelected: (isMultipleTargetSelected) =>\n    set({ isMultipleTargetSelected }),\n}))\n"
  },
  {
    "path": "store/use-resize-canvas.ts",
    "content": "import { create } from 'zustand'\n\ninterface ResizeCanvasState {\n  resolution: string\n  setResolution: (res: string) => void\n\n  domResolution: string\n  setDomResolution: (res: string) => void\n\n  canvasRoundness: number\n  setCanvasRoundness: (canvasRoundness: number) => void\n\n  scrollScale: number\n  setScrollScale: (scrollScale: number) => void\n\n  scaleFactor: number\n  setScaleFactor: (scaleFactor: number) => void\n\n  shouldFloat: boolean\n  setShouldFloat: (shouldFloat: boolean) => void\n\n  automaticResolution: boolean\n  setAutomaticResolution: (automaticResolution: boolean) => void\n\n  exactDomResolution: string\n  setExactDomResolution: (exactDomResolution: string) => void\n}\n\nexport const useResizeCanvas = create<ResizeCanvasState>((set) => ({\n  resolution: '1920x1080',\n  setResolution: (res) => set({ resolution: res }),\n\n  exactDomResolution: '1920x1080',\n  setExactDomResolution: (exactDomResolution) => set({ exactDomResolution }),\n\n  domResolution: '....x....',\n  setDomResolution: (res) => set({ domResolution: res }),\n\n  canvasRoundness: 0,\n  setCanvasRoundness: (canvasRoundness) => set({ canvasRoundness }),\n\n  scrollScale: 1,\n  setScrollScale: (scrollScale) => set({ scrollScale }),\n\n  scaleFactor: 1,\n  setScaleFactor: (scaleFactor) => set({ scaleFactor }),\n\n  shouldFloat: false,\n  setShouldFloat: (shouldFloat) => set({ shouldFloat }),\n\n  automaticResolution: true,\n  setAutomaticResolution: (automaticResolution) => set({ automaticResolution }),\n}))\n"
  },
  {
    "path": "store/use-tiptap.ts",
    "content": "import { create } from 'zustand'\n\ninterface TiptapState {\n  shouldShow: boolean\n  setShouldShow: (shouldShow: boolean) => void\n}\n\nexport const useTiptap = create<TiptapState>()((set) => ({\n  shouldShow: false,\n  setShouldShow: (shouldShow) => set({ shouldShow }),\n}))\n"
  },
  {
    "path": "styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* A reusable h-dynamic-screen which has a fallback of vh if dvh is not supported on the browser */\n@layer base {\n  .h-dynamic-screen {\n    height: 100vh;\n  }\n\n  @supports (height: 100dvh) {\n    .h-dynamic-screen {\n      height: 100dvh;\n    }\n  }\n}\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n\n    --muted: 210 40% 94.4%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n\n    --border: 240 6% 25% / 0.13;\n    --input: 214.3 31.8% 91.4%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n\n    --accent: 240 6% 97%;\n    --accent-foreground: 239 71% 73%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n\n    --ring: 215 20.2% 65.1%;\n\n    --radius: 0.5rem;\n\n    --shadowColor1: #000;\n\n    --gradient-angle: 170deg;\n    --gradient-bg: linear-gradient(\n      var(--gradient-angle),\n      #d8d9dd,\n      #cccdd1 100%\n    );\n    --mesh-bg: '';\n    --solid-bg: '';\n  }\n\n  .dark {\n    --background: 216, 9%, 11%;\n    --foreground: 231, 6%, 77%;\n\n    --muted: 217.2 12.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n\n    --popover: 216, 9%, 11%;\n    --popover-foreground: 231, 6%, 77%;\n\n    --card: 216, 9%, 11%;\n    --card-foreground: 231, 6%, 77%;\n\n    --border: 217.2 11% 15%; /*#22262b*/\n    --input: 217.2 11% 15%;\n\n    --primary: 231, 6%, 77%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 231, 6%, 77%;\n\n    --accent: 216, 15%, 9%;\n    --accent-foreground: 242, 81%, 75%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 0 85.7% 97.3%;\n\n    --ring: 217.2 32.6% 17.5%;\n  }\n}\n\nhtml {\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n\n  scroll-behavior: smooth;\n\n  height: 100%;\n\n  color-scheme: dark;\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n@layer components {\n  .sidebar-option {\n    @apply inline-flex h-[9%] items-center justify-between gap-2 border-b border-border px-4 transition-colors hover:bg-accent;\n  }\n\n  .absolute-center {\n    @apply absolute left-1/2 top-1/2 w-1/2 -translate-x-1/2 -translate-y-1/2 transform;\n  }\n\n  .flex-center {\n    @apply flex items-center justify-center;\n  }\n}\n\n@layer utilities {\n  .add-focus {\n    @apply focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-4 focus:ring-offset-background;\n  }\n}\n\n#canvas-container > div > div > img {\n  cursor: pointer;\n}\n\n#canvas-container > div > div > img:active {\n  cursor: grabbing;\n}\n\n.moveable-control-box {\n  --moveable-color: #7b7cd3 !important;\n}\n\n.circular-slider > div {\n  transition: opacity 0s ease-in 0s !important;\n  font-weight: 600;\n}\n\n.ProseMirror {\n  outline: none !important;\n}\n\n@layer utilities {\n  /* Hide scrollbar for Chrome, Safari and Opera */\n  .no-scrollbar::-webkit-scrollbar {\n    display: none;\n  }\n  /* Hide scrollbar for IE, Edge and Firefox */\n  .no-scrollbar {\n    -ms-overflow-style: none; /* IE and Edge */\n    scrollbar-width: none; /* Firefox */\n  }\n}\n\n#canvas-container img {\n  /* disable all selection of image */\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n\n  /* prevent drag of images */\n  -webkit-user-drag: none;\n  -khtml-user-drag: none;\n  -moz-user-drag: none;\n  -o-user-drag: none;\n}\n\n#font-picker {\n  box-shadow: none !important;\n  border: 1px solid #22262b !important;\n  border-radius: 0.5rem;\n}\n\n#font-picker button {\n  background: #1a1c1f !important;\n  box-shadow: none !important;\n  height: 2.5rem;\n  border-radius: 0.5rem;\n}\n\n.dropdown-font-family {\n  font-size: 14px !important;\n}\n\n.font-list {\n  background: #1a1c1f !important;\n  border-radius: 0.5rem;\n}\n\n#font-picker button .dropdown-icon::before {\n  border-top: 6px solid white !important;\n}\n\n.bubble-menu {\n  display: flex;\n  background-color: #0d0d0d;\n  padding: 0.4rem;\n  border-radius: 0.5rem;\n}\n\n.bubble-menu button {\n  border: none;\n  background: none;\n  color: #fff !important;\n  font-size: 1rem !important;\n  font-weight: 500;\n  padding: 0 0.4rem !important;\n  opacity: 0.6;\n}\n\n.bubble-menu button:hover {\n  opacity: 1;\n}\n\n.bubble-menu button.is-active {\n  opacity: 1;\n}\n\n/* Show placeholder when editor is empty */\n.tiptap p.is-editor-empty:first-child::before {\n  color: #ffffff70;\n  content: attr(data-placeholder);\n  float: left;\n  height: 0;\n  pointer-events: none;\n}\n\n/* Show placeholder on every empty line */\n.tiptap p.is-empty::before {\n  color: #ffffff70;\n  content: attr(data-placeholder);\n  float: left;\n  height: 0;\n  pointer-events: none;\n}\n\n.prose-a {\n  color: #898aeb !important;\n}\n\n#tippy-1 ::selection {\n  background-color: #fff;\n  color: #333;\n}\n\n#tippy-1 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n#tippy-2 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n#tippy-3 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n#tippy-4 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n#tippy-5 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n#tippy-6 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n#tippy-7 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n#tippy-8 {\n  position: relative;\n  transform: translate(0, 50px) !important;\n}\n\n/* custom selection */\n::selection {\n  background-color: #595b96;\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "const { fontFamily } = require('tailwindcss/defaultTheme')\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: ['class'],\n  content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],\n  theme: {\n    container: {\n      center: true,\n      padding: '1.5rem',\n      screens: {\n        xlg: '100%'\n      }\n    },\n    extend: {\n      screens: {\n        xlg: '1100px'\n      },\n      backgroundColor: {\n        primary: '#FBFBFA',\n        secondaryLight: '#F1F4F4',\n        dark: '#181819',\n        formDark: 'hsl(216, 9%, 11%)', //\t#1a1c1f\n        sidebar: '#151515',\n      },\n      textColor: {\n        primary: '#212329',\n        dark: '#C2C3C9',\n        purple: '#898AEB',\n      },\n\n      colors: {\n        border: 'hsl(var(--border))',\n        input: 'hsl(var(--input))',\n        ring: '#8e8ece',\n        background: 'hsl(var(--background))',\n        foreground: 'hsl(var(--foreground))',\n        primary: {\n          DEFAULT: 'hsl(var(--primary))',\n          foreground: 'hsl(var(--primary-foreground))',\n        },\n        secondary: {\n          DEFAULT: 'hsl(var(--secondary))',\n          foreground: 'hsl(var(--secondary-foreground))',\n        },\n        destructive: {\n          DEFAULT: 'hsl(var(--destructive))',\n          foreground: 'hsl(var(--destructive-foreground))',\n        },\n        muted: {\n          DEFAULT: 'hsl(var(--muted))',\n          foreground: 'hsl(var(--muted-foreground))',\n        },\n        accent: {\n          DEFAULT: 'hsl(var(--accent))',\n          foreground: 'hsl(var(--accent-foreground))',\n        },\n        popover: {\n          DEFAULT: 'hsl(var(--popover))',\n          foreground: 'hsl(var(--popover-foreground))',\n        },\n        card: {\n          DEFAULT: 'hsl(var(--card))',\n          foreground: 'hsl(var(--card-foreground))',\n        },\n      },\n      borderRadius: {\n        lg: `var(--radius)`,\n        md: `calc(var(--radius) - 2px)`,\n        sm: 'calc(var(--radius) - 4px)',\n      },\n      fontFamily: {\n        sans: ['var(--font-sans)', ...fontFamily.sans],\n      },\n      boxShadow: {\n        custom: '0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08)',\n      },\n      keyframes: {\n        'accordion-down': {\n          from: { height: 0 },\n          to: { height: 'var(--radix-accordion-content-height)' },\n        },\n        'accordion-up': {\n          from: { height: 'var(--radix-accordion-content-height)' },\n          to: { height: 0 },\n        },\n      },\n      animation: {\n        'accordion-down': 'accordion-down 0.2s ease-out',\n        'accordion-up': 'accordion-up 0.2s ease-out',\n      },\n    },\n  },\n  plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \"build/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "utils/auth-options.ts",
    "content": "import { PrismaAdapter } from '@next-auth/prisma-adapter'\nimport {\n  type NextAuthOptions,\n  type DefaultSession,\n  getServerSession,\n} from 'next-auth'\nimport GithubProvider from 'next-auth/providers/github'\nimport GoogleProvider from 'next-auth/providers/google'\nimport prismadb from '@/libs/prismadb'\n\ndeclare module 'next-auth' {\n  interface Session extends DefaultSession {\n    user: {\n      id: string\n      isCreator: boolean\n      createdAt: string | null\n    } & DefaultSession['user']\n  }\n}\n\ndeclare module 'next-auth/jwt' {\n  interface JWT {\n    id?: string\n    isCreator?: boolean\n    createdAt?: string | null\n  }\n}\n\nexport const authOptions: NextAuthOptions = {\n  pages: {\n    signIn: '/',\n    error: '/error',\n  },\n  adapter: PrismaAdapter(prismadb),\n  providers: [\n    GithubProvider({\n      clientId: process.env.GITHUB_ID!,\n      clientSecret: process.env.GITHUB_SECRET!,\n    }),\n    GoogleProvider({\n      clientId: process.env.GOOGLE_ID!,\n      clientSecret: process.env.GOOGLE_SECRET!,\n    }),\n  ],\n  callbacks: {\n    async jwt({ token, user, session, trigger }) {\n      if (user) {\n        return {\n          ...token,\n          id: user.id,\n          name: user.name,\n          email: user.email,\n          image: user.image,\n          // @ts-expect-error values come from Prisma but not in default type\n          isCreator: user?.isCreator,\n          createdAt: (user as any)?.createdAt ?? null,\n        }\n      }\n\n      // fetch new user data from database when session is updated\n      if (trigger === 'update' && token.id) {\n        const dbUser = await prismadb.user.findUnique({\n          where: { id: token.id as string },\n          select: {\n            name: true,\n            email: true,\n            image: true,\n            isCreator: true,\n            createdAt: true,\n          },\n        })\n\n        if (dbUser) {\n          return {\n            ...token,\n            name: dbUser.name,\n            email: dbUser.email,\n            image: dbUser.image,\n            isCreator: dbUser.isCreator,\n            createdAt: dbUser.createdAt?.toISOString() ?? null,\n          }\n        }\n      }\n\n      return token\n    },\n    async session({ session, token }) {\n      return {\n        ...session,\n        user: {\n          ...session.user,\n          id: token.id as string,\n          name: token.name as string | null,\n          email: token.email as string | null,\n          image: token.image as string | null,\n          isCreator: token.isCreator as boolean,\n          createdAt: token.createdAt as string | null,\n        },\n      }\n    },\n  },\n  session: {\n    strategy: 'jwt',\n  },\n  secret: process.env.NEXTAUTH_SECRET,\n  debug: process.env.NODE_ENV === 'development',\n}\n\n/** Reusable function to eliminate the need of passing authOptions everytime */\nexport const getCurrentSession = () => getServerSession(authOptions)\n"
  },
  {
    "path": "utils/button-utils.ts",
    "content": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "utils/helper-fns.ts",
    "content": "export function splitWidthHeight(resolution: string) {\n  const [width, height] = resolution.split('x')\n  return { width, height }\n}\n\nexport function convertHexToRgba(hexCode: string, opacity = 1) {\n  var hex = hexCode.replace('#', '')\n\n  if (hex.length === 3) {\n    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]\n  }\n\n  var r = parseInt(hex.substring(0, 2), 16),\n    g = parseInt(hex.substring(2, 4), 16),\n    b = parseInt(hex.substring(4, 6), 16)\n\n  /* Backward compatibility for whole number based opacity values. */\n  if (opacity > 1 && opacity <= 100) {\n    opacity = opacity / 100\n  }\n\n  return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')'\n}\n\nexport function calculateEqualCanvasSize(\n  imgWidth: number,\n  imgHeight: number,\n  padding: number\n) {\n  const aspectRatio = imgWidth / imgHeight\n  let canvasWidth, canvasHeight\n\n  if (aspectRatio > 1) {\n    canvasWidth = imgWidth + 2 * padding\n    canvasHeight = imgHeight + 2 * padding\n  } else {\n    canvasHeight = imgHeight + 2 * padding\n    canvasWidth = imgWidth + 2 * padding\n  }\n\n  return `${canvasWidth}x${canvasHeight}`\n}\n\nexport function capitalize(str: string) {\n  if (!str || typeof str !== 'string') return str\n  return str.charAt(0).toUpperCase() + str.slice(1)\n}\n\nexport const truncate = (str: string, length: number) => {\n  if (!str || str.length <= length) return str\n  return `${str.slice(0, length)}...`\n}\n\nexport const separateCommas = (str: string) => {\n  if (!str) return\n\n  const strArr = str.split(',')\n\n  return strArr.map((item) => capitalize(item.trim()))\n}\n\nexport const generateBadgeVariant = (str: string) => {\n  if (!str) return\n\n  const helpVariant = ['help', 'support', 'question', 'faq', 'tutorial', 'guide']\n  const destructiveVariant = [\n    'error',\n    'critical',\n    'important',\n    'caution',\n    'alert',\n    'important',\n  ]\n  const successVariant = [\n    'trends',\n    'announcement',\n    'sale',\n    'new',\n    'update',\n    'feature',\n  ]\n\n  if (helpVariant.includes(str.toLowerCase())) return 'help'\n  if (successVariant.includes(str.toLowerCase())) return 'success'\n  if (destructiveVariant.includes(str.toLowerCase())) return 'destructive'\n  return 'default'\n}\n\nexport function formatDate(date: Date | string | number | undefined): string {\n  // format date as 29 May, 2021\n  if (!date) return 'N/A'\n\n  if (typeof date === 'number' || typeof date === 'string')\n    date = new Date(date)\n\n  const d = new Date(date)\n  const year = d.getFullYear()\n  const month = d.toLocaleString('default', { month: 'short' })\n  const day = d.getDate()\n\n  return `${day} ${month}, ${year}`\n}\n\nexport function generateFormattedBlogDate(\n  createdAt: string | Date,\n  updatedAt: string | Date\n): string {\n  const createdDate: Date = new Date(createdAt)\n  const updatedDate: Date = new Date(updatedAt)\n\n  // Check if the dates are the same\n  const datesAreEqual: boolean = createdDate.getTime() === updatedDate.getTime()\n\n  // Format the date string\n  const formattedDate = (date: Date): string => {\n    const options: Intl.DateTimeFormatOptions = {\n      year: 'numeric',\n      month: 'long',\n      day: 'numeric',\n    }\n    return date.toLocaleDateString('en-US', options)\n  }\n\n  if (datesAreEqual) {\n    // If the dates are the same, show 'Published on'\n    return `Published on ${formattedDate(createdDate)}`\n  } else {\n    // If the dates are different, show 'Updated on'\n    return `Updated on ${formattedDate(updatedDate)}`\n  }\n}\n"
  },
  {
    "path": "utils/presets/gradients.ts",
    "content": "export interface Gradient {\n  type: 'Normal' | 'Mesh'\n  gradient: string\n  background?: string\n}\n\nexport const gradients: Gradient[] = [\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #DFE0E2, #E3E9F1 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #c7dae1, #fafbff 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(119, 136, 153), rgb(211, 211, 211) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(202, 194, 255), rgb(242, 231, 248) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(165, 210, 202), rgb(248, 241, 241) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(246, 244, 231), rgb(248, 201, 195) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #a3a8f2, #fafbff 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(177, 173, 219) 11.2%, rgb(245, 226, 226) 91.1%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(116,126,175),rgb(190,141,141))',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(204, 228, 247) 11.2%, rgb(237, 246, 250) 100.2%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), #e0c3fc 0%, #8ec5fc 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(255, 182, 148), rgb(171, 199, 242) 50%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(218, 185, 252) 11.2%, rgb(125, 89, 252) 91.1%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), #A9C9FF 0%, #FFBBEC 100%)',\n  },\n\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #86e3ce, #d0e6a5 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(230,203,156), rgb(219,183,105))',\n  },\n\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #898aeb, #d8b9e3)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #82d9c5, #ccaae2 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(71, 140, 118), rgb(138, 206, 144) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), #7F7FD5, #86A8E7, #91EAE4)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), #dbeafe, #93c5fd, #3b82f6)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(26, 98, 138), rgb(255, 254, 255) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), #f9a8d4, #d8b4fe, #818cf8)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #4e54c8, #8f94fb)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #a8c0ff, #3f2b96)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #c4b5fd, #a78bfa)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #ee9ca7, #ffdde1)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(244, 63, 94), rgb(248, 113, 113), rgb(239, 68, 68))',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(251, 113, 133), rgb(253, 186, 116))',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), #f0abfc, #f5f5f4, #fef08a)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #d2d0f5, #f8a9da 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #4675c0, #8fc8eb 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #49bcf6, #49deb2 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle),#4CA1AF,#C4E0E5)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #ff7d58, #ffebff 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle),#EECDA3,#EF629F)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(221, 153, 51), rgb(248, 233, 190) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(245, 126, 217), rgb(255, 236, 162) 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient: 'linear-gradient(var(--gradient-angle), #19335a, #8fc8ea 100%)',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(40,39,42), rgb(40,39,41),rgb(116,164,212),rgb(238,238,238),rgb(134,107,127))',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(40,39,42), rgb(40,39,41),rgb(116,164,212))',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(40,39,42), rgb(116,126,175),rgb(190,141,141))',\n  },\n  {\n    type: 'Normal',\n    gradient:\n      'linear-gradient(var(--gradient-angle), rgb(142, 211, 217) 15%, rgb(238, 67, 244) 50%, rgb(76, 83, 167) 85%)',\n  },\n  {\n    type: 'Mesh',\n    background: 'rgb(249, 168, 212)',\n    gradient:\n      'radial-gradient(at 95% 85%, rgb(217, 249, 157) 0, transparent 100%), radial-gradient(at 49% 91%, rgb(255, 228, 230) 0, transparent 86%), radial-gradient(at 44% 80%, rgb(191, 219, 254) 0, transparent 21%), radial-gradient(at 62% 12%, rgb(254, 202, 202) 0, transparent 75%), radial-gradient(at 66% 75%, rgb(39, 39, 42) 0, transparent 38%), radial-gradient(at 42% 66%, rgb(253, 164, 175) 0, transparent 66%)',\n  },\n  {\n    type: 'Mesh',\n    background: 'rgb(255, 153, 221)',\n    gradient:\n      'radial-gradient(at 14% 29%, rgb(240, 96, 214) 0px, transparent 50%), radial-gradient(at 74% 11%, rgb(238, 114, 172) 0px, transparent 50%), radial-gradient(at 8% 98%, rgb(232, 74, 93) 0px, transparent 50%), radial-gradient(at 48% 27%, rgb(185, 138, 239) 0px, transparent 50%), radial-gradient(at 16% 82%, rgb(248, 109, 230) 0px, transparent 50%), radial-gradient(at 10% 91%, rgb(177, 67, 84) 0px, transparent 50%), radial-gradient(at 5% 23%, rgb(152, 145, 227) 0px, transparent 50%)',\n  },\n  {\n    type: 'Mesh',\n    background: 'rgb(255, 153, 167)',\n    gradient:\n      'radial-gradient(at 85% 2%, rgb(132, 85, 252) 0px, transparent 50%), radial-gradient(at 14% 1%, rgb(165, 93, 223) 0px, transparent 50%), radial-gradient(at 35% 19%, rgb(73, 177, 233) 0px, transparent 50%), radial-gradient(at 81% 33%, rgb(72, 231, 234) 0px, transparent 50%), radial-gradient(at 66% 14%, rgb(233, 198, 144) 0px, transparent 50%), radial-gradient(at 81% 91%, rgb(253, 240, 139) 0px, transparent 50%), radial-gradient(at 68% 71%, rgb(243, 104, 150) 0px, transparent 50%)',\n  },\n  {\n    type: 'Mesh',\n    background: 'rgb(244, 114, 182)',\n    gradient:\n      'radial-gradient(at 46% 10%, rgb(254, 249, 195) 0px, transparent 79%)',\n  },\n  {\n    type: 'Mesh',\n    background: 'rgb(7, 89, 133)',\n    gradient:\n      'radial-gradient(at 49% 17%, rgb(134, 239, 172) 0px, transparent 87%), radial-gradient(at 0% 75%, rgb(110, 231, 183) 0px, transparent 100%)',\n  },\n  {\n    type: 'Mesh',\n    background: 'rgb(217, 70, 239)',\n    gradient:\n      'radial-gradient(at 5% 66%, rgb(254, 226, 226) 0px, transparent 89%), radial-gradient(at 63% 98%, rgb(207, 250, 254) 0px, transparent 41%), radial-gradient(at 42% 55%, rgb(126, 34, 206) 0px, transparent 76%), radial-gradient(at 72% 46%, rgb(234, 179, 8) 0px, transparent 86%), radial-gradient(at 35% 23%, rgb(162, 28, 175) 0px, transparent 35%), radial-gradient(at 39% 61%, rgb(15, 118, 110) 0px, transparent 23%)',\n  },\n]\n"
  },
  {
    "path": "utils/presets/qualities.ts",
    "content": "export interface Quality {\n  quality: string\n  value: number\n}\n\nexport const qualities = [\n  { quality: '0.5x Low', value: 0.5 },\n  { quality: '1x Default', value: 1 },\n  { quality: '2x QHD', value: 2 },\n  { quality: '4x UHD', value: 4 },\n]"
  },
  {
    "path": "utils/presets/resolutions.ts",
    "content": "export interface Resolution {\n  name: string\n  resolutions?: Array<{\n    preset: string\n    resolution: string\n  }>\n  icon?: string\n  color?: string\n}\n\nexport const resolutions: Resolution[] = [\n  { name: 'Fit' },\n  { name: 'Equal padding' },\n  {\n    name: 'Facebook',\n    resolutions: [\n      {\n        preset: 'News feed',\n        resolution: '1200x1200',\n      },\n      {\n        preset: 'Story',\n        resolution: '1080x1920',\n      },\n      {\n        preset: 'Event',\n        resolution: '1920x1005',\n      },\n    ],\n    icon: 'Facebook',\n    color: '#1877F2',\n  },\n  {\n    name: 'Youtube',\n    resolutions: [\n      {\n        preset: 'Thumbnail',\n        resolution: '1280x720',\n      },\n      {\n        preset: 'Profile',\n        resolution: '800x800',\n      },\n      {\n        preset: 'Banner',\n        resolution: '1546x423',\n      },\n    ],\n    icon: 'Youtube',\n    color: '#c4302b',\n  },\n  {\n    name: 'Instagram',\n    resolutions: [\n      {\n        preset: 'Feed',\n        resolution: '1080x1080',\n      },\n      {\n        preset: 'Portrait',\n        resolution: '1080x1350',\n      },\n      {\n        preset: 'Story/Reels',\n        resolution: '1080x1920',\n      },\n    ],\n    icon: 'Instagram',\n    color: '#d62976',\n  },\n  {\n    name: 'Twitter',\n    resolutions: [\n      {\n        preset: 'Post',\n        resolution: '2400x1350',\n      },\n      {\n        preset: 'Header',\n        resolution: '1500x500',\n      },\n    ],\n    icon: 'Twitter',\n    color: '#2E9DEA',\n  },\n  {\n    name: 'LinkedIn',\n    resolutions: [\n      {\n        preset: 'Post',\n        resolution: '1200x1200',\n      },\n      {\n        preset: 'Banner',\n        resolution: '1584x396',\n      },\n    ],\n    icon: 'LinkedIn',\n    color: '#0077b5',\n  },\n  {\n    name: 'Dribble',\n    resolutions: [\n      {\n        preset: 'Shot',\n        resolution: '1600x1200',\n      },\n      {\n        preset: 'Shot 2k',\n        resolution: '2800x2100',\n      },\n    ],\n    icon: 'Dribble',\n    color: '#D83A79',\n  },\n  {\n    name: 'ProductHunt',\n    resolutions: [\n      {\n        preset: 'Gallery',\n        resolution: '1270x760',\n      },\n    ],\n    icon: 'ProductHunt',\n    color: '#D55124',\n  },\n]\n"
  },
  {
    "path": "utils/presets/shadows.ts",
    "content": "export interface Shadow {\n  name: string\n  fullName: string\n  shadow: string\n  preview: string\n}\n\nexport const shadows: Shadow[] = [\n  {\n    name: 'none',\n    fullName: 'None',\n    shadow: `0 0 0 0`,\n    preview: `0 0 0 0`,\n  },\n  {\n    name: 'sm',\n    fullName: 'Small',\n    shadow: `0px 6px 20px`,\n    preview: `0px 6px 20px #00000080`,\n  },\n  {\n    name: 'md',\n    fullName: 'Medium',\n    shadow: ``,\n    preview: `0px 6px 20px #000000`,\n  },\n  {\n    name: 'bottom',\n    fullName: 'Bottom',\n    shadow: `0 25px 50px -12px`,\n    preview: `0 25px 50px -12px #000000`,\n  },\n  {\n    name: 'lg',\n    fullName: 'Large',\n    shadow: `0px 10px 60px 0px`,\n    preview: `0px 6px 30px 0px #000000`,\n  },\n  {\n    name: 'x-lg',\n    fullName: 'X-Large',\n    shadow: `0px 19px 60px 10px`,\n    preview: `0px 10px 60px 0px #000000`,\n  },\n]"
  },
  {
    "path": "utils/presets/solid-colors.ts",
    "content": "export const solidColors = [\n  {\n    background: 'transparent',\n  },\n  {\n    background: '#cac2ff',\n  },\n  {\n    background: '#ff9e9e',\n  },\n  {\n    background: '#f9a8d4',\n  },\n  {\n    background: '#e8e2ba',\n  },\n  {\n    background: '#d9f99d',\n  },\n  {\n    background: '#90ee90',\n  },\n  {\n    background: '#99bdff',\n  },\n  {\n    background: '#E1E0E1',\n  },\n  {\n    background: '#D8D9DD',\n  },\n]\n"
  },
  {
    "path": "utils/tiptap-extensions.ts",
    "content": "import StarterKit from '@tiptap/starter-kit'\nimport Image from '@tiptap/extension-image'\nimport { Underline } from '@tiptap/extension-underline'\nimport { Link } from '@tiptap/extension-link'\nimport { Placeholder } from '@tiptap/extension-placeholder'\n\nexport const extensions = [\n  StarterKit.configure({\n    bulletList: {\n      keepMarks: true,\n      keepAttributes: false,\n    },\n    orderedList: {\n      keepMarks: true,\n      keepAttributes: false,\n    },\n  }),\n  Underline.configure({\n    HTMLAttributes: {\n      class: 'my-custom-class',\n    },\n  }),\n  Link.configure({\n    // openOnClick: true,\n    linkOnPaste: true,\n  }),\n  Placeholder.configure({\n    placeholder: ({ node }) => {\n      if (node.type.name === 'heading') {\n        return 'Write heading...'\n      }\n\n      return 'Write something...'\n    },\n\n    showOnlyWhenEditable: false,\n  }),\n  Image,\n]\n"
  },
  {
    "path": "workers/background-removal.worker.ts",
    "content": "self.onmessage = async (event: MessageEvent<{ src: string }>) => {\n  const { src } = event.data\n\n  if (!src) {\n    self.postMessage({ type: 'error', error: 'No image source provided.' })\n    return\n  }\n\n  try {\n    const { removeBackground } = await import('@imgly/background-removal')\n\n    const blob = await removeBackground(src, {\n      output: {\n        format: 'image/png',\n        quality: 1,\n      },\n      device: 'gpu',\n    })\n    const url = URL.createObjectURL(blob)\n    self.postMessage({ type: 'success', url })\n  } catch (error: unknown) {\n    const errorMessage =\n      error instanceof Error\n        ? error.message\n        : 'Unknown error removing background in worker.'\n    console.error('Worker error:', error)\n    self.postMessage({ type: 'error', error: errorMessage })\n  }\n}\n"
  }
]