Full Code of Sls0n/Prismify for AI

main 339ac7a56cf4 cached
155 files
412.6 KB
109.8k tokens
167 symbols
1 requests
Download .txt
Showing preview only (451K chars total). Download the full file or copy to clipboard to get everything.
Repository: Sls0n/Prismify
Branch: main
Commit: 339ac7a56cf4
Files: 155
Total size: 412.6 KB

Directory structure:
gitextract_iv9ddlq0/

├── .eslintrc.json
├── .gitignore
├── .vscode/
│   └── settings.json
├── README.md
├── app/
│   ├── (routes)/
│   │   ├── about/
│   │   │   └── page.tsx
│   │   ├── articles/
│   │   │   ├── [slug]/
│   │   │   │   ├── loading.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── page.tsx
│   │   │   └── sitemap.ts
│   │   └── layout.tsx
│   ├── admin/
│   │   ├── layout.tsx
│   │   └── write-article/
│   │       ├── blog-form.tsx
│   │       └── page.tsx
│   ├── api/
│   │   ├── article/
│   │   │   └── post/
│   │   │       └── route.ts
│   │   ├── auth/
│   │   │   └── [...nextauth]/
│   │   │       └── route.ts
│   │   └── user/
│   │       └── settings/
│   │           └── route.ts
│   ├── error.tsx
│   ├── layout.tsx
│   ├── loading.tsx
│   ├── manifest.json
│   ├── not-found.tsx
│   ├── page.tsx
│   ├── robots.ts
│   └── sitemap.ts
├── components/
│   ├── articles/
│   │   └── article-card.tsx
│   ├── auth-modal.tsx
│   ├── clarity-script.tsx
│   ├── color-picker.tsx
│   ├── editor/
│   │   ├── background-image-canvas.tsx
│   │   ├── background-options/
│   │   │   ├── custom-gradient-picker.tsx
│   │   │   ├── image-gradient-picker.tsx
│   │   │   ├── index.tsx
│   │   │   ├── noise-slider.tsx
│   │   │   ├── normal-gradient-picker.tsx
│   │   │   └── pattern-picker.tsx
│   │   ├── browser-frames.tsx
│   │   ├── canvas-area.tsx
│   │   ├── canvas-options/
│   │   │   ├── canvas-roundness-slider.tsx
│   │   │   ├── index.tsx
│   │   │   └── resolution-button.tsx
│   │   ├── frame-options/
│   │   │   ├── additional-frame-options.tsx
│   │   │   ├── frame-picker.tsx
│   │   │   └── index.tsx
│   │   ├── image-context-menu.tsx
│   │   ├── image-options/
│   │   │   ├── add-image-button.tsx
│   │   │   ├── index.tsx
│   │   │   ├── inset-option.tsx
│   │   │   ├── roundness-option.tsx
│   │   │   ├── scale-options.tsx
│   │   │   └── shadow-settings.tsx
│   │   ├── main-image-area.tsx
│   │   ├── mobile-view-image-options.tsx
│   │   ├── moveable-component.tsx
│   │   ├── noise.tsx
│   │   ├── perspective-options/
│   │   │   ├── index.tsx
│   │   │   └── rotate-options.tsx
│   │   ├── position-options/
│   │   │   ├── index.tsx
│   │   │   ├── position-control.tsx
│   │   │   └── translate-control.tsx
│   │   ├── selecto-component.tsx
│   │   ├── sidebar-buttons.tsx
│   │   ├── sidebar.tsx
│   │   ├── text-context-menu.tsx
│   │   ├── text-layers.tsx
│   │   ├── text-options/
│   │   │   ├── add-text-layer.tsx
│   │   │   ├── font-settings.tsx
│   │   │   └── index.tsx
│   │   ├── tiptap-moveable.tsx
│   │   └── undo-redo-buttons.tsx
│   ├── export-options.tsx
│   ├── footer.tsx
│   ├── icons/
│   │   ├── index.tsx
│   │   └── info.icon.tsx
│   ├── loader.tsx
│   ├── navbar.tsx
│   ├── navlinks.tsx
│   ├── popup-color-picker.tsx
│   ├── profile-dialog.tsx
│   ├── settings-dialog.tsx
│   ├── sign-in-form.tsx
│   ├── spinner/
│   │   ├── spinner.module.css
│   │   └── spinner.tsx
│   ├── ui/
│   │   ├── accordion.tsx
│   │   ├── back-button.tsx
│   │   ├── badge.tsx
│   │   ├── button.tsx
│   │   ├── circular-slider.tsx
│   │   ├── context-menu.tsx
│   │   ├── dialog.tsx
│   │   ├── drawer.tsx
│   │   ├── dropdown-menu.tsx
│   │   ├── gradient-text.tsx
│   │   ├── input.tsx
│   │   ├── popover.tsx
│   │   ├── scroll-area.tsx
│   │   ├── select.tsx
│   │   ├── separator.tsx
│   │   ├── skeleton.tsx
│   │   ├── slider.tsx
│   │   ├── spotlight-button.tsx
│   │   ├── style/
│   │   │   └── checkbox.module.css
│   │   ├── switch.tsx
│   │   ├── tabs.tsx
│   │   ├── text-area.tsx
│   │   ├── text.tsx
│   │   ├── theme-button.tsx
│   │   ├── toast.tsx
│   │   ├── toaster.tsx
│   │   └── tooltip.tsx
│   └── user-dropdown.tsx
├── components.json
├── docker-compose.yml
├── hooks/
│   ├── canvas-area-hooks/
│   │   ├── use-automatic-aspect-ratio-switcher.ts
│   │   ├── use-resize-observer.ts
│   │   └── use-screen-size-warning-toast.ts
│   ├── use-editor.ts
│   ├── use-event-listener.ts
│   ├── use-isomorphic-layout-effect.ts
│   ├── use-media-query.ts
│   ├── use-on-click-outside.ts
│   └── use-toast.ts
├── index.d.ts
├── libs/
│   ├── prismadb.ts
│   └── validators/
│       ├── article-post-validator.ts
│       └── user-settings-validator.ts
├── license.md
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── prettier.config.js
├── prisma/
│   └── schema.prisma
├── providers/
│   └── index.tsx
├── store/
│   ├── use-active-index.ts
│   ├── use-background-options.ts
│   ├── use-color-extractor.ts
│   ├── use-frame-options.ts
│   ├── use-image-options.ts
│   ├── use-image-quality.ts
│   ├── use-moveable.ts
│   ├── use-resize-canvas.ts
│   └── use-tiptap.ts
├── styles/
│   └── globals.css
├── tailwind.config.js
├── tsconfig.json
├── utils/
│   ├── auth-options.ts
│   ├── button-utils.ts
│   ├── helper-fns.ts
│   ├── presets/
│   │   ├── gradients.ts
│   │   ├── qualities.ts
│   │   ├── resolutions.ts
│   │   ├── shadows.ts
│   │   └── solid-colors.ts
│   └── tiptap-extensions.ts
└── workers/
    └── background-removal.worker.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.json
================================================
{
  "extends": "next/core-web-vitals"
}


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

Checkpoint.tsx

================================================
FILE: .vscode/settings.json
================================================
{
  "tailwindCSS.experimental.classRegex": [
    ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
  ],
  "editor.fontLigatures": true,
  "editor.renderWhitespace": "none",

  "tailwindCSS.includeLanguages": {
    "html": "html",
    "javascript": "javascript",
    "typescript": "typescript",
    "css": "css"
  },
  "editor.quickSuggestions": {
    "strings": true
  },
  "typescript.tsserver.experimental.enableProjectDiagnostics": true
}


================================================
FILE: README.md
================================================
## Prismify

Prismify is a web app that aims to revitalize & enhance boring images/screenshots. With prismify, you can effortlessly enhance your images/screenshots.

![Prismify](https://github.com/Sls0n/Prismify/assets/102340248/d37df848-59da-4e26-8dbc-451562ef6c55)


## Preview
![image](https://github.com/Sls0n/Prismify/assets/102340248/5e004ca7-d53a-400e-993c-a34cd9bdc829)
![prismify_ss](https://github.com/Sls0n/Prismify/assets/102340248/33323217-59ba-48a1-a494-09fa8658f354)


## Tech Stacks

- Typescript
- Next
- React
- Tailwind
- Prisma
- Zustand

## Perfect lighthouse score
![prismify-render-1704521997749](https://github.com/Sls0n/Prismify/assets/102340248/1f268d3e-cd9b-4d88-88da-247607ccbc45)

## Setup
Run the following command to install dependencies and generate the Prisma client:

```bash
pnpm run setup
```





================================================
FILE: app/(routes)/about/page.tsx
================================================
/* eslint-disable react/no-unescaped-entities */
import React from 'react'
import BackButton from '@/components/ui/back-button'
import { Text } from '@/components/ui/text'
import type { Metadata } from 'next'
import Image from 'next/image'
import { GradientText } from '@/components/ui/gradient-text'

export const metadata: Metadata = {
  title: 'About - Prismify',
  description: 'Read details about Prismify.',
  openGraph: {
    title: 'About - Prismify',
    description: 'Read details about Prismify.',
  },
  alternates: {
    canonical: 'https://prismify.vercel.app/about',
  },
}

export default function page() {
  return (
    <section className="container flex w-full flex-col gap-8 pt-[72px] lg:gap-16">
      <div className="mt-8">
        <BackButton />
      </div>

      <article className="mx-auto h-fit w-full max-w-prose rounded-md">
        <div className="mb-16 flex w-full items-start justify-between">
          <div className="mx-auto flex flex-col gap-3">
            {/* Title */}
            <GradientText
              as="h1"
              variant="purple"
              className="line-clamp-2 border-b-[3px] border-[#898aeb] text-center text-[2rem] font-semibold capitalize leading-tight md:text-[2.8rem]"
            >
              <span className="!font-medium">About</span> &mdash; Prismify
            </GradientText>
          </div>
        </div>

        <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">
          <figure className="">
            <Image
              width={1280}
              height={720}
              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"
              alt="About Prismify"
              className="aspect-auto max-w-full object-cover"
            />
          </figure>
          <p>
            Hey there, fellow entrepreneurs! Ever felt like banging your head
            against the wall trying to create eye-catching designs for your
            product or social media? I hear you! But guess what? Prismify's here
            to save the day.
          </p>{' '}
          <h3 className="text-dark">
            Let's dive into the struggles we all face:
          </h3>
          <ul>
            <li>
              <strong>Zero Design Skills:</strong> Who has time to become a
              design pro? With Prismify, you don't need to! It's like having a
              magic wand for your visuals.
            </li>
            <li>
              <strong>Clock's Ticking:</strong> Time is money, they say.
              Prismify respects that – swift, efficient, and no-nonsense design
              tools.
            </li>
            <li>
              <strong>Quality Matters:</strong> We all want our stuff to look
              top-notch. Prismify keeps your visuals sharp and professional
              without the designer price tag.
            </li>
          </ul>
          <p>
            Now, hold on to your hats! Here's how Prismify makes design a
            cakewalk:
          </p>
          <ul>
            <li>
              Browser Frame Mockups: Pop your product into a fancy browser frame
              in a snap!
            </li>
            <li>
              Custom Gradients: Gradient backgrounds that'll make your graphics
              pop like fireworks! With also adaptive gradients support!
            </li>
            <li>
              Text Tricks & Notes: Jazz up your visuals with text enhancements
              and handy annotations.
            </li>
            <li>
              Color Magic & 3D Vibes: Prismify's color game and 3D effects?
              Mind-blowing.
            </li>
            <li>
              Hi-Res & Lightning-Speed Edits: Get crisp, high-res images without
              the wait. Lightning-fast editing? You got it!
            </li>
          </ul>
          <p>Perks for You:</p>
          <ul>
            <li>
              Time Saver: Spend less time stressing over designs, more time
              making things happen.
            </li>
            <li>
              Pro-Level Designs: No more 'amateur hour' visuals. Prismify gives
              you that polished, pro look.
            </li>
            <li>
              Versatility Rules: Social media, websites, product shots –
              Prismify's got your back for any visual need.
            </li>
          </ul>
          <p>Feeling the design itch? Scratch it with Prismify!</p>
          <blockquote>
            "Transform Your Visuals Today! Try Prismify – Your Visual Upgrade
            Shortcut."
          </blockquote>
          <p>
            Wrapping it Up: Prismify's not just a tool; it's your secret weapon
            for killer SaaS visuals and social media. Remember to show, not just
            tell – demos and examples make all the difference. Get ready to rock
            those visuals!
          </p>
        </div>
      </article>
    </section>
  )
}


================================================
FILE: app/(routes)/articles/[slug]/loading.tsx
================================================
import Loader from '@/components/loader'

export default function Loading() {
  return <Loader />
}


================================================
FILE: app/(routes)/articles/[slug]/page.tsx
================================================
import BackButton from '@/components/ui/back-button'
import { Badge } from '@/components/ui/badge'
import { Text } from '@/components/ui/text'
import prismadb from '@/libs/prismadb'
import {
  generateBadgeVariant,
  generateFormattedBlogDate,
  separateCommas,
} from '@/utils/helper-fns'
import { Calendar } from 'lucide-react'
import type { Metadata } from 'next'
import Image from 'next/image'
import { notFound } from 'next/navigation'
import { cache } from 'react'

type ArticleProps = {
  params: Promise<{
    slug: string
  }>
}

export const revalidate = 3600 // 1 hour

export async function generateStaticParams() {
  const blogs = await prismadb.article.findMany({
    select: {
      slug: true,
    },
  })

  return blogs.map((blog) => ({
    slug: blog.slug,
  }))
}

const getBlog = cache(async (slug: string) => {
  const blog = await prismadb.article.findFirst({
    where: {
      slug,
    },
  })

  return blog
})

export async function generateMetadata(props: ArticleProps) {
  const params = await props.params;
  const blog = await getBlog(params.slug)

  if (!blog) {
    return
  }

  const metadata: Metadata = {
    title: `${blog.title} - Prismify`,
    description: blog.summary,
    openGraph: {
      title: `${blog.title} - Prismify`,
      description: blog?.summary || 'N/A',
      locale: 'en_US',
      url: `https://prismify.vercel.app/articles/${blog.slug}`,
      type: 'article',
      images: [
        {
          url:
            blog.imageUrl ?? 'https://prismify.vercel.app/opengraph-image.jpg',
          width: 1280,
          height: 720,
          alt: blog.title,
        },
      ],
    },
    twitter: {
      creator: '@xSls0n_007',
      title: `${blog.title} - Prismify`,
      description: blog.summary || 'N/A',
      card: 'summary_large_image',
      images: [
        {
          url:
            blog.imageUrl ?? 'https://prismify.vercel.app/opengraph-image.jpg',
          width: 1280,
          height: 720,
          alt: blog.title,
        },
      ],
    },
    alternates: {
      canonical: `https://prismify.vercel.app/articles/${blog.slug}`,
    },
    publisher: 'Prismify',
  }

  return metadata
}

export default async function ArticlePage(props: ArticleProps) {
  const params = await props.params;
  const blog = await getBlog(params.slug)

  if (!blog) {
    return notFound()
  }

  return (
    <section className="container flex w-full flex-col gap-8 pt-[72px] lg:gap-16">
      <div className="mt-8">
        <BackButton />
      </div>

      <article className="mx-auto h-fit w-full max-w-prose rounded-md">
        <div className="mb-16 flex w-full items-start justify-between">
          <div className="flex flex-col gap-3">
            {/* Time published/updated */}
            <div className="flex items-center">
              <Calendar className="mr-2 h-[1.15rem] w-[1.15rem] text-white/40" />
              <time
                className="text-base font-normal text-dark/60"
                dateTime={
                  blog?.updatedAt.toISOString() ?? blog?.createdAt.toISOString()
                }
              >
                {generateFormattedBlogDate(blog?.createdAt, blog?.updatedAt)}
              </time>
            </div>

            {/* Title */}
            <Text
              variant="h1"
              bold
              className="text-[2rem] font-bold capitalize leading-tight text-dark md:text-[2.8rem]"
            >
              {blog.title ?? 'N/A'}
            </Text>
            {/* Category */}
            {blog?.category && (
              <div className="flex items-center gap-2">
                {separateCommas(blog?.category)!.map((category) => (
                  <Badge
                    key={category}
                    variant={generateBadgeVariant(category)}
                  >
                    {category}
                  </Badge>
                ))}
              </div>
            )}
          </div>
        </div>

        <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">
          {blog?.imageUrl && (
            <figure className="h-fit overflow-hidden rounded-lg bg-formDark">
              <Image
                src={blog?.imageUrl ?? '/images/fallback.jpg'}
                alt={'cover image'}
                width={1280}
                height={720}
                priority
                className="aspect-auto max-w-full object-cover"
              />

              {/* <figcaption>
              Picture
            </figcaption> */}
            </figure>
          )}
          {blog?.content && (
            <div
              dangerouslySetInnerHTML={{
                __html: blog.content,
              }}
            ></div>
          )}
        </div>
      </article>
    </section>
  )
}


================================================
FILE: app/(routes)/articles/page.tsx
================================================
import prismadb from '@/libs/prismadb'
import { Text } from '@/components/ui/text'
import ArticleCard from '@/components/articles/article-card'
import { formatDate, separateCommas } from '@/utils/helper-fns'
import type { Metadata } from 'next'
import { unstable_cache as cache } from 'next/cache'

export const metadata: Metadata = {
  title: 'Articles - Prismify',
  description: 'Read latest articles from Prismify.',
  openGraph: {
    title: 'Articles - Prismify',
    description: 'Read latest articles from Prismify.',
    type: 'article',
    url: 'https://prismify.vercel.app/articles',
  },
  alternates: {
    canonical: 'https://prismify.vercel.app/articles',
  },
}

const getCachedArticles = cache(
  async () => {
    return await prismadb.article.findMany({
      orderBy: {
        createdAt: 'desc',
      },
    })
  },
  ['articles'],
  {
    revalidate: 60 * 60, // 1 hour
    tags: ['articles'],
  }
)

export default async function Article() {
  const articles = await getCachedArticles()

  return (
    <section className="w-full flex-1 pt-[72px]">
      <div className="mb-16 flex h-48 w-full flex-col items-center justify-center bg-black/10">
        <Text
          variant="h1"
          bold
          className="mb-2 line-clamp-2 text-[2rem] font-bold capitalize leading-tight tracking-normal text-dark md:text-[2.8rem]"
        >
          Articles
        </Text>

        <Text
          variant="bodyMedium"
          className="line-clamp-2 flex items-center gap-1 text-dark/70 "
        >
          <span>Read latest articles from</span>
          <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">
            Prismify
          </span>
        </Text>
      </div>

      <ul
        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"
        role="list"
      >
        {articles.length === 0 ? (
          <div className="mb-24 text-center">
            <Text variant="bodyMedium" className="text-dark/70">
              &mdash; The list is empty. Go back to previous page
            </Text>
          </div>
        ) : (
          articles.map((article) => {
            return (
              <div key={article.id} className="relative col-span-1 row-span-1">
                <ArticleCard
                  title={article.title ?? 'N/A'}
                  image={article.imageUrl ?? '/images/fallback.jpg'}
                  date={formatDate(article.updatedAt) ?? 'N/A'}
                  href={`/articles/${article.slug}`}
                  category={
                    separateCommas(article?.category ?? 'Design')?.at(0) ?? ''
                  }
                />
              </div>
            )
          })
        )}
      </ul>
    </section>
  )
}


================================================
FILE: app/(routes)/articles/sitemap.ts
================================================
import prismadb from '@/libs/prismadb'
import { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const response = await prismadb.article.findMany({
    select: {
      slug: true,
      updatedAt: true,
    },
  })

  const sitemapUrls: MetadataRoute.Sitemap = response?.map((p) => {
    return {
      url: `https://prismify.vercel.app/articles/${p.slug}`,
      lastModified: new Date(p.updatedAt),
      changeFrequency: 'monthly',
    }
  })

  return sitemapUrls
}


================================================
FILE: app/(routes)/layout.tsx
================================================
import Footer from '@/components/footer'
import React from 'react'

export default function AdminLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <main className="flex h-full w-full flex-col">
      {children}

      <Footer />
    </main>
  )
}


================================================
FILE: app/admin/layout.tsx
================================================
import React from 'react'

export default function AdminLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <main className="container h-full w-full">
      {children}
    </main>
  )
}


================================================
FILE: app/admin/write-article/blog-form.tsx
================================================
'use client'

import { EditorProvider, useCurrentEditor } from '@tiptap/react'
import {
  Bold,
  Code,
  Heading,
  ImagePlus,
  Italic,
  Link,
  List,
  ListOrdered,
  Quote,
  Space,
  Strikethrough,
  Underline,
} from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
// import { FileUpload } from '@/components/file-upload'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { cn } from '@/utils/button-utils'
import { extensions } from '@/utils/tiptap-extensions'
import { useMutation } from '@tanstack/react-query'
import { Text } from '@/components/ui/text'
import { useTiptap } from '@/store/use-tiptap'
import axios, { AxiosError } from 'axios'
import { toast } from '@/hooks/use-toast'
import { Textarea } from '@/components/ui/text-area'

export default function BlogForm() {
  const [blogOutput, setBlogOutput] = useState<string>('')
  const [title, setTitle] = useState<string>('')
  const [summary, setSummary] = useState<string>('')
  const [category, setCategory] = useState<string>('')
  const [slug, setSlug] = useState<string>('')
  const [mainBlogImg, setMainBlogImg] = useState<string>('')

  const { mutate: publishBlog, isLoading: isPublishing } = useMutation({
    mutationFn: async () => {
      const res = await axios.post('/api/article/post', {
        title,
        summary,
        category,
        slug,
        imageUrl: mainBlogImg,
        content: blogOutput,
      })

      return res
    },
    onSuccess: () => {
      toast({
        title: 'Blog published successfully',
      })
    },
    onError: (err) => {
      if (err instanceof AxiosError) {
        if (err.response?.status === 400) {
          toast({
            title: err?.response?.data?.[0]?.message,
            variant: 'destructive',
          })
        }

        if (err.response?.status === 401) {
          toast({
            title: 'You are not authorized to publish this blog',
            description: 'Please login to publish this blog',
            variant: 'destructive',
          })
        }

        if (err.response?.status === 409) {
          toast({
            title: 'Blog with this slug already exists',
            description: 'Please modify the slug a bit.',
            variant: 'destructive',
          })
        }

        if (err.response?.status === 500) {
          toast({
            title: 'Something went wrong',
            description: 'Please try again later',
            variant: 'destructive',
          })
        }
      }
      console.log(err)
    },
  })

  const publishBlogHandler = () => {
    publishBlog()
  }

  const slugifyTitle = async (title: string) => {
    const slugify = (await import('slugify')).default
    setSlug(slugify(title, { lower: true, strict: true }))
  }

  return (
    <>
      <div className="mb-8 mt-8 space-y-6 rounded-lg border-[1.5px] border-border bg-[#151515] p-8">
        <div className="space-y-2">
          <Text className="text-dark/90" variant="bodyLarge" semibold>
            Title
          </Text>

          <Input
            placeholder="Write title for your blog..."
            className="h-16 border-transparent bg-[#151515] p-0 placeholder:text-dark/50 focus-visible:ring-0 focus-visible:ring-transparent md:text-xl "
            value={title}
            onChange={(e) => {
              setTitle(e.target.value)
              slugifyTitle(e.target.value)
            }}
            required
          />
        </div>

        <div className="space-y-2">
          <Text className="text-dark/90" variant="bodyLarge" semibold>
            Summary
          </Text>

          <Textarea
            placeholder="Summary of the blog..."
            className="bg-transparent h-32 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
            value={summary}
            onChange={(e) => setSummary(e.target.value)}
          />
        </div>

        <div className="space-y-2">
          <Text className="text-dark/90" variant="bodyLarge" semibold>
            Category (Comma separated)
          </Text>

          <Input
            placeholder="Eg: Design, Marketing"
            className="h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
            value={category}
            onChange={(e) => setCategory(e.target.value)}
          />
        </div>

        <div className="space-y-2">
          <Text className="text-dark/90" variant="bodyLarge" semibold>
            Slug
          </Text>

          <Input
            placeholder="Eg: blog-title-slug"
            className="h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
            value={slug}
            onChange={(e) => setSlug(e.target.value)}
          />
        </div>

        <div className="space-y-2">
          <Text className="text-dark/90" variant="bodyLarge" semibold>
            Image URL
          </Text>

          <Input
            placeholder="Eg: https://images.unsplash.com/photo/..."
            className="h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
            value={mainBlogImg}
            onChange={(e) => setMainBlogImg(e.target.value)}
          />
        </div>
      </div>

      <div className="mb-8 mt-4 w-full space-y-6 rounded-lg border-[1.5px] border-border bg-[#151515] p-8">
        <div className="w-full ">
          <Text variant="bodyXLarge" className="pb-4 text-dark/90" semibold>
            Write your blog here
          </Text>

          <hr className="-mx-8 border-border pb-4" />

          <EditorProvider
            slotBefore={<MenuBar />}
            extensions={extensions}
            parseOptions={{
              preserveWhitespace: true,
            }}
            editorProps={{
              attributes: {
                class:
                  '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',
              },
            }}
            onUpdate={(content) =>
              setBlogOutput(content?.editor?.getHTML())
            }
          />
        </div>
      </div>

      <Button
        isLoading={isPublishing}
        onClick={publishBlogHandler}
        variant="default"
        className="mb-24 max-w-[10rem]"
      >
        <Text variant="bodyMedium" semibold>
          Publish blog
        </Text>
      </Button>
    </>
  )
}

const MenuBar = () => {
  const { editor } = useCurrentEditor()

  const addImage = useCallback(() => {
    const url = window.prompt('URL')

    if (url) {
      editor?.chain().focus().setImage({ src: url }).run()
    }
  }, [editor])

  const setLink = useCallback(() => {
    const previousUrl = editor?.getAttributes('link').href as string
    const url = window.prompt('URL', previousUrl)

    // cancelled
    if (url === null) {
      return
    }

    // empty
    if (url === '') {
      editor?.chain().focus().extendMarkRange('link').unsetLink().run()

      return
    }

    // update link
    editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
  }, [editor])

  return (
    <div className="sticky top-[4.5rem] z-50 bg-[#151515]">
      <div className="z-50 flex flex-wrap gap-y-4 bg-[#151515] pb-5 pt-2 backdrop-blur-md">
        <Button
          variant="menuItem"
          onClick={() =>
            editor?.chain().focus().toggleHeading({ level: 3 }).run()
          }
          disabled={
            !editor?.can().chain().focus().toggleHeading({ level: 3 }).run()
          }
          className={cn(
            editor?.isActive('heading', { level: 3 })
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : 'border border-transparent',
            'mr-2 px-2.5 text-foreground/80 '
          )}
        >
          <Heading className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleBold().run()}
          disabled={!editor?.can().chain().focus().toggleBold().run()}
          className={cn(
            editor?.isActive('bold')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : 'border border-transparent',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <Bold className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleItalic().run()}
          disabled={!editor?.can().chain().focus().toggleItalic().run()}
          className={cn(
            editor?.isActive('italic')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : 'border border-transparent',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <Italic className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleUnderline().run()}
          disabled={!editor?.can().chain().focus().toggleUnderline().run()}
          className={cn(
            editor?.isActive('underline')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : 'border border-transparent',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <Underline className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleStrike().run()}
          disabled={!editor?.can().chain().focus().toggleStrike().run()}
          className={cn(
            editor?.isActive('strike')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : 'border border-transparent',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <Strikethrough className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleCodeBlock().run()}
          disabled={!editor?.can().chain().focus().toggleCodeBlock().run()}
          className={cn(
            editor?.isActive('codeBlock')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : 'border border-transparent',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <Code className="h-5 w-5" />
        </Button>

        <div className="my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10"></div>

        <Button
          className="mr-2 px-2.5 text-foreground/80"
          variant="menuItem"
          onClick={addImage}
        >
          <ImagePlus className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => {
            editor?.isActive('link')
              ? editor?.chain().focus().unsetLink().run()
              : setLink()
          }}
          className={cn(
            editor?.isActive('link')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : '',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <Link className="h-5 w-5" />
        </Button>

        <div className="my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10"></div>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleBulletList().run()}
          disabled={!editor?.can().chain().focus().toggleBulletList().run()}
          className={cn(
            editor?.isActive('bulletList')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : '',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <List className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleOrderedList().run()}
          disabled={!editor?.can().chain().focus().toggleOrderedList().run()}
          className={cn(
            editor?.isActive('orderedList')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : '',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <ListOrdered className="h-5 w-5" />
        </Button>

        <div className="my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10"></div>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().toggleBlockquote().run()}
          disabled={!editor?.can().chain().focus().toggleBlockquote().run()}
          className={cn(
            editor?.isActive('blockquote')
              ? 'border border-indigo-400/10 bg-indigo-400/10'
              : '',
            'mr-2 px-2.5 text-foreground/80'
          )}
        >
          <Quote className="h-5 w-5" />
        </Button>

        <Button
          variant="menuItem"
          onClick={() => editor?.chain().focus().setHorizontalRule().run()}
          className={cn('mr-2 px-2.5 text-foreground/80')}
        >
          <Space className="h-5 w-5" />
        </Button>
      </div>
      <hr className="-mx-8 border-border pb-10" />
    </div>
  )
}


================================================
FILE: app/admin/write-article/page.tsx
================================================
import Loader from '@/components/loader'
import BackButton from '@/components/ui/back-button'
import dynamic from 'next/dynamic'
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Admin - Write Article',
  description: 'Write an article for the blog.',
}

const BlogForm = dynamic(() => import('./blog-form'), {
  loading: () => <Loader />,
})

export default function page() {
  return (
    <section className="mx-auto mt-6 h-[100dvh] w-full max-w-prose pt-[72px] lg:mt-8">
      <BackButton />
      <BlogForm />
    </section>
  )
}


================================================
FILE: app/api/article/post/route.ts
================================================
import { z } from 'zod'
import { NextResponse } from 'next/server'
import prismadb from '@/libs/prismadb'
import { getCurrentSession } from '@/utils/auth-options'
import { postSchema } from '@/libs/validators/article-post-validator'
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const { title, summary, category, slug, content, imageUrl } =
      postSchema.parse(body)

    const session = await getCurrentSession()

    if (!session || !session.user.isCreator) {
      return new NextResponse('Unauthorized', { status: 401 })
    }

    const slugExits = await prismadb.article.findFirst({
      where: { slug },
    })

    if (slugExits) {
      return new NextResponse('Article already exists', { status: 409 })
    }

    const blog = await prismadb.article.create({
      data: {
        title,
        summary,
        category,
        slug,
        content,
        imageUrl,
      },
    })

    revalidateTag('articles')

    return new NextResponse('OK')
  } catch (error) {
    console.log(error)
    console.log(error)
    if (error instanceof z.ZodError) {
      return new NextResponse(error.message, { status: 400 })
    }
    return new NextResponse('Something went wrong', { status: 500 })
  }
}


================================================
FILE: app/api/auth/[...nextauth]/route.ts
================================================
import { AuthOptions } from 'next-auth'
import { authOptions } from '@/utils/auth-options'
import NextAuth from 'next-auth'

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }


================================================
FILE: app/api/user/settings/route.ts
================================================
import { z } from 'zod'
import { NextResponse } from 'next/server'
import prismadb from '@/libs/prismadb'
import { getCurrentSession } from '@/utils/auth-options'
import { userSettingsSchema } from '@/libs/validators/user-settings-validator'

export async function GET() {
  try {
    const session = await getCurrentSession()
    if (!session) {
      return new NextResponse('Unauthorized', { status: 401 })
    }

    const dbUser = await prismadb.user.findUnique({
      where: { id: session.user.id },
      select: {
        id: true,
        name: true,
        email: true,
        image: true,
        isCreator: true,
        createdAt: true,
      },
    })

    return NextResponse.json({
      session: {
        id: session.user.id,
        name: session.user.name,
        email: session.user.email,
        image: session.user.image,
        isCreator: session.user.isCreator,
      },
      database: dbUser,
    })
  } catch (error) {
    return new NextResponse('Something went wrong', { status: 500 })
  }
}

export async function PATCH(request: Request) {
  try {
    const session = await getCurrentSession()
    if (!session) {
      return new NextResponse('Unauthorized', { status: 401 })
    }

    const body = await request.json()
    const { name, image } = userSettingsSchema.parse(body)

    const updateData = {
      name: name ?? undefined,
      image: image ?? undefined,
    }

    await prismadb.user.update({
      where: { id: session.user.id },
      data: updateData,
    })

    return new NextResponse('OK')
  } catch (error) {
    if (error instanceof z.ZodError) {
      return new NextResponse(error.message, { status: 400 })
    }
    return new NextResponse('Something went wrong', { status: 500 })
  }
}


================================================
FILE: app/error.tsx
================================================
'use client'

/* eslint-disable react/no-unescaped-entities */
import { Button, buttonVariants } from '@/components/ui/button'
import { cn } from '@/utils/button-utils'
import { MailWarning, RotateCcw } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    console.error(error)
  }, [error])

  return (
    <div className="flex flex-col items-center px-4 pt-40 text-center sm:px-6 lg:px-8">
      <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 ">
        Something went amiss.
      </h1>
      <br />
      <p className="max-w-xl text-center text-lg font-medium text-foreground opacity-90 md:text-xl">
        <strong className="font-semibold">Sorry!</strong>{' '}
        {error.message ? error.message : 'Looks like prismify needs glasses :('}
      </p>

      <div className="mt-10 flex flex-col items-center justify-center gap-2 sm:flex-row sm:gap-3">
        <Button onClick={reset} className="w-48 text-base sm:w-fit" variant="stylish">
          <RotateCcw className="mr-2 h-5 w-5 text-foreground/80" />
          Pray & Retry
        </Button>
        <Link
          href="mailto:silson0072@gmail.com"
          className={cn(
            buttonVariants({ variant: 'default' }),
            'w-48 px-4 text-base sm:w-fit'
          )}
        >
          <MailWarning className="mr-2 h-5 w-5 text-foreground/80" />
          Report
        </Link>
      </div>
    </div>
  )
}


================================================
FILE: app/layout.tsx
================================================
import ClarityScript from '@/components/clarity-script'
import Navbar from '@/components/navbar'
import { Toaster } from '@/components/ui/toaster'
import Providers from '@/providers'
import '@/styles/globals.css'
import { cn } from '@/utils/button-utils'
import { Analytics } from '@vercel/analytics/react'
import type { Metadata, Viewport } from 'next'
import { Plus_Jakarta_Sans } from 'next/font/google'

const Font = Plus_Jakarta_Sans({ subsets: ['latin'] })

export const metadata: Metadata = {
  title:
    'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
  description:
    '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.',
  verification: {
    google: 'cum1ckoCozAtSA3Xcn4UX_xR_FlfrlzKzQRa7nYQ2YM',
  },
  icons: {
    icon: '/favicons/favicon.ico',
    apple: '/favicons/apple-touch-icon.png',
  },
  applicationName: 'Prismify',
  creator: 'Slson',
  twitter: {
    creator: '@xSls0n_007',
    title:
      'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
    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.',
    card: 'summary_large_image',
    images: [
      {
        url: 'https://prismify.vercel.app/opengraph-image.jpg',
        width: 1200,
        height: 630,
        alt: 'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
      },
    ],
  },
  openGraph: {
    title:
      'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
    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.',
    siteName: 'Prismify',
    url: 'https://prismify.vercel.app',
    locale: 'en_US',
    type: 'website',
    images: [
      {
        url: 'https://prismify.vercel.app/opengraph-image.jpg',
        width: 1200,
        height: 630,
        alt: 'Prismify - Effortlessly Create Beautiful SaaS/Product Shots & Graphics',
      },
    ],
  },
  category: 'Design',
  alternates: {
    canonical: 'https://prismify.vercel.app',
  },
  keywords: [
    'SaaS product design',
    'Create beautiful screenshots',
    'Graphics design for social media',
    'Add browser frames',
    'Gradient backgrounds for graphics',
    'Text annotations tool',
    'Beautiful website graphics',
    'Enhance social media visuals',
    'Website demo image creator',
    'Visual content enhancement',
    'SaaS branding tool',
    'Design beautiful image for products',
    'Prismify graphics maker',
    'SaaS graphics design',
    'Website screenshot generator',
    'SaaS marketing visuals',
  ],
  metadataBase: new URL('https://prismify.vercel.app'),
  robots: 'index, follow',
  manifest: '/manifest.json',
}

export const viewport: Viewport = {
  themeColor: '#151515',
  colorScheme: 'dark',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html
      suppressHydrationWarning
      className={cn('dark antialiased', Font.className)}
      lang="en"
    >
      <body className="min-h-screen bg-[#111] text-dark">
        <Toaster />
        <Analytics />

        <Providers>
          <Navbar />
          {children}
        </Providers>

        <ClarityScript />
      </body>
    </html>
  )
}


================================================
FILE: app/loading.tsx
================================================
import Loader from '@/components/loader'

export default function Loading() {
  return <Loader />
}


================================================
FILE: app/manifest.json
================================================
{
  "name": "Prismify",
  "short_name": "Prismify",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#111111",
  "theme_color": "#151515",
  "description": "Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics.",
  "icons": [
    {
      "src": "/favicons/apple-icon.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "/favicons/favicon.ico",
      "sizes": "48x48 64x64 96x96 128x128 256x256",
      "type": "image/x-icon"
    }
  ]
}


================================================
FILE: app/not-found.tsx
================================================
/* eslint-disable react/no-unescaped-entities */
'use client'

import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/utils/button-utils'
import { MoveLeft } from 'lucide-react'
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="flex flex-col items-center px-4 pt-40 text-center sm:px-6 lg:px-8">
      <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 ">
        Lost in the Pixels.
      </h1>
      <br />
      <p className="max-w-xl text-center text-lg font-medium text-foreground opacity-90 md:text-xl">
        <strong className="font-semibold">Not found!</strong> Looks like you've
        wandered off the grid.
      </p>

      <div className="mt-10 flex flex-col items-center justify-center gap-2 sm:flex-row sm:gap-3">
        <Link
          href="/"
          className={cn(
            buttonVariants({ variant: 'default' }),
            'w-48 px-4 text-base sm:w-fit'
          )}
        >
          <MoveLeft className="mr-2 h-5 w-5 text-foreground/80" />
          Back to home
        </Link>
      </div>
    </div>
  )
}


================================================
FILE: app/page.tsx
================================================
import Sidebar from '@/components/editor/sidebar'
import Canvas from '@/components/editor/canvas-area'

export default function Home() {
  return (
    <main className="flex h-screen w-screen pt-[72px] sm:flex-row">
      <Sidebar />
      <Canvas />
    </main>
  )
}


================================================
FILE: app/robots.ts
================================================
import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: '/admin/',
    },
    sitemap: [
      'https://prismify.vercel.app/sitemap.xml',
      'https://prismify.vercel.app/articles/sitemap.xml',
    ],
  }
}


================================================
FILE: app/sitemap.ts
================================================
import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  const sitemapUrls: MetadataRoute.Sitemap = allStaticRoutes.map((p) => {
    return {
      url: `https://prismify.vercel.app/${p}`,
      lastModified: '2024-01-13T12:17:43.023Z',
      changeFrequency: p === '' ? 'monthly' : 'weekly',
    }
  })

  return sitemapUrls
}

const allStaticRoutes = ['', 'about', 'articles']


================================================
FILE: components/articles/article-card.tsx
================================================
import Image from 'next/image'
import Link from 'next/link'

type ArticleCardProps = {
  title: string
  category: string
  image: string
  date: string
  href: string
}

export default function ArticleCard({
  title,
  image,
  date,
  category,
  href,
}: ArticleCardProps) {
  return (
    <div className="group  flex flex-col items-center text-dark">
      <Link
        href={href}
        className="h-[300px] w-full overflow-hidden rounded-xl bg-formDark"
      >
        <Image
          src={image}
          alt={title}
          width={500}
          height={300}
          className="aspect-[4/3] h-full w-full object-cover object-center transition-all will-change-auto duration-300 group-hover:scale-105"
        />
      </Link>

      <div className="mt-4 flex w-full flex-col">
        <span className="text-xs font-semibold uppercase text-purple sm:text-sm">
          {category}
        </span>
        <Link href={href} className="my-1.5 inline-block">
          <h2 className="line-clamp-2 text-base font-semibold capitalize text-dark sm:text-lg">
            {title}
          </h2>
        </Link>

        <span className="dark:text-light/50 max-w-xs text-sm font-medium capitalize text-dark/60  sm:text-base">
          {date}
        </span>
      </div>
    </div>
  )
}


================================================
FILE: components/auth-modal.tsx
================================================
import * as React from 'react'

import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import { useMediaQuery } from '@/hooks/use-media-query'
import { LogIn } from 'lucide-react'
import SignInForm from './sign-in-form'

export function AuthModal() {
  const [open, setOpen] = React.useState(false)
  const isDesktop = useMediaQuery('(min-width: 768px)')

  if (isDesktop) {
    return (
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogTrigger asChild>
          <Button
            className="rounded-xl text-[13.6px]"
            size="sm"
            variant="default"
          >
            <p className="hidden md:block">Login</p>
            <LogIn size={18} className="flex-center md:ml-2" />
          </Button>
        </DialogTrigger>
        <DialogContent className="scale-110 p-16">
          <SignInForm />
        </DialogContent>
      </Dialog>
    )
  }

  return (
    <Drawer open={open} onOpenChange={setOpen}>
      <DrawerTrigger asChild>
        <Button
          className="rounded-xl text-[13.6px]"
          size="sm"
          variant="default"
        >
          <p className="hidden md:block">Login</p>
          <LogIn size={18} className="flex-center md:ml-2" />
        </Button>
      </DrawerTrigger>
      <DrawerContent className="mb-6 rounded-xl bg-[#121212] px-6 py-2">
        <div className="mt-8" />
        <SignInForm />
        <div className="mt-4" />
      </DrawerContent>
    </Drawer>
  )
}


================================================
FILE: components/clarity-script.tsx
================================================
import Script from 'next/script'

const ClarityScript = () => (
  <Script id="clarity-script" strategy="afterInteractive">
    {`
            (function(c,l,a,r,i,t,y){
                c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
                t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
                y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
            })(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID}");
          `}
  </Script>
)

export default ClarityScript


================================================
FILE: components/color-picker.tsx
================================================
'use client'

import { useState } from 'react'
import {
  HexAlphaColorPicker,
  HexColorInput,
  HexColorPicker,
} from 'react-colorful'

export default function ColorPicker({
  onChange,
  colorState,
  shouldShowAlpha = true,
}: {
  onChange: (color: string) => void
  colorState: string
  shouldShowAlpha?: boolean
}) {
  const [color, setColor] = useState(colorState)

  return (
    <>
      <div className="flex-center flex-col gap-2">
        {shouldShowAlpha ? (
          <HexAlphaColorPicker
            color={color}
            onChange={(color) => {
              setColor(color)
              onChange(color)
            }}
          />
        ) : (
          <HexColorPicker
            color={color}
            onChange={(color) => {
              setColor(color)
              onChange(color)
            }}
          />
        )}
        <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">
          <span className="absolute left-2 font-medium text-gray-400">#</span>
          <HexColorInput
            color={color}
            onChange={(color) => {
              setColor(color)
              onChange(color)
            }}
            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"
          />
        </div>
      </div>
    </>
  )
}


================================================
FILE: components/editor/background-image-canvas.tsx
================================================
// this component shows background image & noise in the canvas

import { useBackgroundOptions } from '@/store/use-background-options'
import Noise from '@/components/editor/noise'

export default function BackgroundImageCanvas() {
  const { imageBackground } = useBackgroundOptions()

  return (
    <>
      {imageBackground && (
        // eslint-disable-next-line @next/next/no-img-element
        <img
          draggable={false}
          className={`pointer-events-none absolute z-[0] h-full w-full object-cover`}
          src={imageBackground}
          alt="background image"
        />
      )}
      <Noise />
    </>
  )
}


================================================
FILE: components/editor/background-options/custom-gradient-picker.tsx
================================================
'use client'

import { useCallback } from 'react'
import { solidColors } from '@/utils/presets/solid-colors'
import { Button } from '@/components/ui/button'
import PopupColorPicker from '@/components/popup-color-picker'
import { useBackgroundOptions } from '@/store/use-background-options'

export default function CustomGradientPicker() {
  const {
    setBackground,
    background,
    setBackgroundType,
    solidColor,
    setSolidColor,
    setImageBackground,
    imageBackground,
  } = useBackgroundOptions()

  const updateRootStyles = useCallback((color: string) => {
    if (typeof window === 'undefined') return
    document?.documentElement.style.setProperty('--solid-bg', color)
    document?.documentElement.style.setProperty('--gradient-bg', color)
    document?.documentElement.style.setProperty('--mesh-bg', color)
  }, [])

  const handleColorChange = useCallback(
    (color: string) => {
      setBackgroundType('solid')
      setSolidColor(color)
      setBackground(color)
      setImageBackground(null)
      updateRootStyles(color)
    },
    [
      setSolidColor,
      setBackground,
      setBackgroundType,
      updateRootStyles,
      setImageBackground,
    ]
  )

  return (
    <>
      <div>
        <h3 className="mb-3 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
          <span>Pick a color:</span>
        </h3>
        <PopupColorPicker onChange={handleColorChange} color={solidColor} />
      </div>

      <div>
        <h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
          <span>Solid Colors:</span>
        </h3>

        <div className="mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid w-full">
          {solidColors.slice(0, 1).map(({ background: solidBackground }) => {
            return (
              <Button
                key={solidBackground}
                className={`aspect-square overflow-hidden rounded-sm p-0 ${
                  background === solidBackground &&
                  !imageBackground &&
                  'outline-none ring-2 ring-ring ring-offset-2'
                }`}
                onClick={() => handleColorChange(solidBackground)}
                style={{ background: solidBackground }}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  className="h-full w-full scale-150"
                  src="/images/transparent.jpg"
                  alt="transparent background"
                />
              </Button>
            )
          })}
          {solidColors.slice(1).map(({ background: solidBackground }) => {
            return (
              <Button
                key={solidBackground}
                variant="secondary"
                className={`h-[2.56rem] w-[2.56rem] rounded-md ${
                  background === solidBackground &&
                  !imageBackground &&
                  'outline-none ring-2 ring-ring ring-offset-2'
                }`}
                onClick={() => handleColorChange(solidBackground)}
                style={{ background: solidBackground }}
              />
            )
          })}
        </div>
      </div>
    </>
  )
}


================================================
FILE: components/editor/background-options/image-gradient-picker.tsx
================================================
'use client'

import { Button } from '@/components/ui/button'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'
import { Skeleton } from '@/components/ui/skeleton'
import { Switch } from '@/components/ui/switch'
import { toast } from '@/hooks/use-toast'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useQuery } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight, Settings2 } from 'lucide-react'
import { Key, useEffect, useState } from 'react'

export default function ImageGradientPicker() {
  const {
    setImageBackground,
    imageBackground,
    setAttribution,
    setHighResBackground,
    highResBackground,
    setBackgroundType,
  } = useBackgroundOptions()
  const [currentPage, setCurrentPage] = useState(1)

  const fetchUnsplashPictures = async (page: number) => {
    const response = await fetch(
      `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}`
    )
    const data = await response.json()
    return data
  }

  const {
    isLoading,
    isError,
    refetch,
    data: unsplashData,
    error,
  } = useQuery({
    queryKey: ['unsplash-gradients'],
    queryFn: () => fetchUnsplashPictures(currentPage),
  })

  useEffect(
    () => {
      refetch()
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [currentPage]
  )

  if (isLoading) {
    const skeletonLoaders = Array.from({ length: 30 }).map((_, index) => (
      <li
        className={`h-[2.56rem] w-[2.56rem] rounded-md`}
        key={`skeleton-${index}`}
      >
        <Skeleton className="h-full w-full rounded-md" />
      </li>
    ))

    return (
      <>
        <h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
          <span>Images:</span>
        </h3>
        <ul className="mt-4 grid max-w-[18rem] auto-rows-auto grid-cols-5 gap-4">
          {skeletonLoaders}
        </ul>
      </>
    )
  }

  if (isError && error instanceof Error) {
    toast({
      title: 'Error',
      description: error.message,
      variant: 'destructive',
    })
    return <span>Error: {error.message}</span>
  }

  return (
    <>
      <h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
        <span>Images:</span>
        <Popover>
          <PopoverTrigger asChild>
            <Settings2 size={20} />
          </PopoverTrigger>
          <PopoverContent className="flex w-fit flex-wrap gap-3">
            <h1 className="text-[0.85rem]">High resolution background</h1>
            <Switch
              checked={highResBackground}
              onCheckedChange={(checked) => {
                setHighResBackground(checked)
              }}
            />
          </PopoverContent>
        </Popover>
      </h3>

      <ul className="mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid">
        {unsplashData.map(
          (data: {
            user: any
            links: any
            id: Key | null | undefined
            urls: {
              regular: string | null
              small_s3: string | undefined
              full: string | undefined
            }
            alt_description: string | undefined
          }) => (
            <li className={`h-[2.56rem] w-[2.56rem] rounded-md`} key={data.id}>
              <button
                className={`h-full w-full rounded-md ${
                  imageBackground ===
                    (highResBackground
                      ? `${data.urls.full}`
                      : `${data.urls.regular}`) &&
                  'outline-none ring-2 ring-ring ring-offset-2'
                }`}
                onClick={() => {
                  setBackgroundType('gradient')
                  setImageBackground(
                    highResBackground
                      ? `${data.urls.full}`
                      : `${data.urls.regular}`
                  )
                  setAttribution({
                    name: data.user.first_name,
                    link: data.user.username,
                  })
                  // Just triggering a download (Unsplash guideline)
                  fetch(
                    `${data.links.download_location}&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`
                  )
                }}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  className="h-full w-full rounded-md object-cover"
                  src={data.urls.small_s3!}
                  alt={data.alt_description}
                />
              </button>
            </li>
          )
        )}
      </ul>

      <div className="flex justify-end mt-6 gap-2">
        <Button
          size="sm"
          variant={'stylish'}
          disabled={currentPage === 1}
          className="text-sm flex-center h-9 px-2.5"
          onClick={() => {
            setCurrentPage((prevPage) => prevPage - 1)
          }}
        >
          <ChevronLeft size={16} className="mr-1 translate-y-[1px]" />
          <p>Back</p>
        </Button>
        <Button
          variant={'stylish'}
          disabled={currentPage === 3}
          className="text-sm flex-center h-9 px-2.5"
          onClick={() => {
            setCurrentPage((prevPage) => prevPage + 1)
          }}
        >
          <p>Next</p> 
          <ChevronRight size={16} className="ml-1 translate-y-[1px]" />
        </Button>
      </div>
    </>
  )
}


================================================
FILE: components/editor/background-options/index.tsx
================================================
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useBackgroundOptions } from '@/store/use-background-options'
import CustomGradientPicker from './custom-gradient-picker'
import NormalGradientPicker from './normal-gradient-picker'
import NoiseSlider from './noise-slider'
import PatternPicker from './pattern-picker'
import { useEffect } from 'react'

export default function BackgroundOptions() {
  const { backgroundType, setIsBackgroundClicked } = useBackgroundOptions()

  useEffect(() => {
    setIsBackgroundClicked(true)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  return (
    <Tabs
      className="mt-4 !border-none !shadow-none !bg-transparent w-full"
      defaultValue={
        backgroundType === 'solid'
          ? 'customTab'
          : backgroundType === 'pattern'
          ? 'patternsTab'
          : 'gradientsTab'
      }
    >
      <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">
        <TabsTrigger value="gradientsTab">Gradient</TabsTrigger>
        <TabsTrigger value="patternsTab">Abstract</TabsTrigger>
        <TabsTrigger className='hidden sm:flex' value="customTab">
          Pick
        </TabsTrigger>
      </TabsList>
      <NoiseSlider />
      <TabsContent value="gradientsTab">
        <NormalGradientPicker />
      </TabsContent>
      <TabsContent value="patternsTab">
        <PatternPicker />
      </TabsContent>
      <TabsContent value="customTab">
        <CustomGradientPicker />
      </TabsContent>
    </Tabs>
  )
}


================================================
FILE: components/editor/background-options/noise-slider.tsx
================================================
import { Slider } from '@/components/ui/slider'
import { useBackgroundOptions } from '@/store/use-background-options'
import { Grip } from 'lucide-react'

export default function NoiseSlider() {
  const { noise, setNoise } = useBackgroundOptions()

  return (
    <div>
      <div className="mb-3 mt-4 flex items-center px-1">
        <h3 className="mb-3 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
          <Grip size={18} />
          <span>Noise</span>
        </h3>
      </div>

      <div className="mb-3 flex gap-4 text-[0.85rem]">
        <Slider
          defaultValue={[0]}
          max={0.8}
          min={0}
          step={0.05}
          value={[noise]}
          onValueChange={(value: number[]) => {
            setNoise(value[0])
          }}
          onIncrement={() => {
            if (noise >= 0.8) return
            setNoise(noise + 0.05)
          }}
          onDecrement={() => {
            if (noise <= 0) return
            setNoise(noise - 0.05)
          }}
        />
      </div>
    </div>
  )
}


================================================
FILE: components/editor/background-options/normal-gradient-picker.tsx
================================================
'use client'

import { useState } from 'react'

import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import CircularSliderComp from '@/components/ui/circular-slider'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'
import SpotlightButton from '@/components/ui/spotlight-button'
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@/components/ui/tooltip'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { gradients, type Gradient } from '@/utils/presets/gradients'
import ColorThief from 'colorthief'
import { Settings2, Sparkles, Zap, Palette, Wand2, Check, Rocket } from 'lucide-react'
import { useCallback } from 'react'
import ImageGradientPicker from './image-gradient-picker'
import { cn } from '@/utils/button-utils'

type Color = string

export default function NormalGradientPicker() {
  const {
    setBackground,
    background: backgroundInStore,
    setBackgroundType,
    backgroundType,
    setImageBackground,
    imageBackground,
    setAttribution,
    setNoise,
  } = useBackgroundOptions()

  const { images, setImages } = useImageOptions()
  const { selectedImage } = useSelectedLayers()
  const [dominantColor, setDominantColor] = useState(null)
  const [isGenerating, setIsGenerating] = useState(false)

  const handleGradientClick = useCallback(
    (gradient: Gradient, isMesh: boolean) => {
      if (typeof window === 'undefined') return
      document?.documentElement.style.setProperty(
        '--gradient-bg',
        gradient.gradient
      )
      document?.documentElement.style.setProperty(
        '--mesh-bg',
        isMesh ? gradient.background! : gradient.gradient
      )
      setBackground(gradient.gradient)
      setBackgroundType(isMesh ? 'mesh' : 'gradient')
      setImageBackground(null)
      setAttribution({ name: null, link: null })
    },
    [setBackground, setBackgroundType, setImageBackground, setAttribution]
  )

  const extractDominantColor = async () => {
    setIsGenerating(true)
    const colorThief = new ColorThief()
    const image = new Image()
    image.crossOrigin = 'Anonymous'
    image.src =
      document.getElementById(`img-${selectedImage}`)?.getAttribute('src') ?? ''

    image.onload = function () {
      const palletes = colorThief.getPalette(image, 8)
      const dominantColor = colorThief.getColor(image)

      function generateGradients(
        dominantColor: Color,
        colors: Color[]
      ): { linearGradients: string[]; radialGradients: string[]; meshGradients: string[] } {
        const getRandomColors = (limit: number): Color[] => {
          const randomColors: Color[] = []
          for (let i = 0; i < limit; i++) {
            const randomIndex = Math.floor(Math.random() * colors.length)
            randomColors.push(colors[randomIndex])
          }
          return randomColors
        }

        // Linear Gradients - Clean, directional gradients with 2-3 colors
        const generateLinearGradients = (): string[] => {
          const gradients: string[] = []
          
          // Dominant to each palette color
          colors.forEach((color: Color, index: number) => {
            gradients.push(`linear-gradient(${135 + index * 30}deg, ${dominantColor} 0%, ${color} 100%)`)
          })
          
          // Two-color combinations from palette
          for (let i = 0; i < 4; i++) {
            const [color1, color2] = getRandomColors(2)
            gradients.push(`linear-gradient(${45 + i * 45}deg, ${color1} 0%, ${color2} 100%)`)
          }
          
          // Three-color smooth transitions
          for (let i = 0; i < 3; i++) {
            const [color1, color2, color3] = getRandomColors(3)
            gradients.push(`linear-gradient(${90 + i * 60}deg, ${color1} 0%, ${color2} 50%, ${color3} 100%)`)
          }
          
          return gradients.slice(0, 14)
        }

        // Radial Gradients - Circular, spotlight effects
        const generateRadialGradients = (): string[] => {
          const gradients: string[] = []
          
          // Center spotlight with dominant color
          colors.forEach((color: Color) => {
            gradients.push(`radial-gradient(circle at 50% 50%, ${dominantColor} 0%, ${color} 100%)`)
          })
          
          // Off-center radials for dynamic feel
          const positions = ['20% 30%', '80% 20%', '30% 80%', '70% 70%']
          positions.forEach((pos, i) => {
            const [color1, color2] = getRandomColors(2)
            gradients.push(`radial-gradient(circle at ${pos}, ${color1} 0%, ${color2} 60%, transparent 100%)`)
          })
          
          // Elliptical radials
          for (let i = 0; i < 3; i++) {
            const [color1, color2] = getRandomColors(2)
            gradients.push(`radial-gradient(ellipse at ${50 + i * 20}% 50%, ${color1} 0%, ${color2} 100%)`)
          }
          
          return gradients.slice(0, 14)
        }

        // Mesh Gradients - Complex, multi-layered beautiful gradients
        const generateMeshGradients = (): string[] => {
          const gradients: string[] = []
          
          // Beautiful mesh gradients with multiple radial layers
          for (let i = 0; i < 14; i++) {
            const meshColors = getRandomColors(4)
            const positions = [
              { x: 10 + Math.random() * 30, y: 10 + Math.random() * 30 },
              { x: 60 + Math.random() * 30, y: 10 + Math.random() * 30 },
              { x: 10 + Math.random() * 30, y: 60 + Math.random() * 30 },
              { x: 60 + Math.random() * 30, y: 60 + Math.random() * 30 }
            ]
            
            const meshGradient = `
              radial-gradient(circle at ${positions[0].x}% ${positions[0].y}%, ${meshColors[0]} 0%, transparent 50%),
              radial-gradient(circle at ${positions[1].x}% ${positions[1].y}%, ${meshColors[1]} 0%, transparent 50%),
              radial-gradient(circle at ${positions[2].x}% ${positions[2].y}%, ${meshColors[2]} 0%, transparent 50%),
              radial-gradient(circle at ${positions[3].x}% ${positions[3].y}%, ${meshColors[3]} 0%, transparent 50%),
              linear-gradient(${45 + i * 15}deg, ${dominantColor} 0%, ${meshColors[0]} 100%)
            `.trim()
            
            gradients.push(meshGradient)
          }
          
          return gradients
        }

        const linearGradients = generateLinearGradients()
        const radialGradients = generateRadialGradients()
        const meshGradients = generateMeshGradients()

        setImages(
          images.map((image, index) =>
            index === images.length - 1
              ? {
                  ...image,
                  dominantColor,
                  palletes,
                  linearGradients,
                  meshGradients,
                  radialGradients,
                }
              : image
          )
        )

        setTimeout(() => setIsGenerating(false), 800)

        return {
          linearGradients,
          radialGradients,
          meshGradients,
        }
      }

      generateGradients(
        // @ts-ignore
        `rgb(${dominantColor.join(',')})`,
        // @ts-ignore
        palletes.map((pallete) => `rgb(${pallete.join(',')})`)
      )
    }
  }

  const GradientSection = ({ title, gradients, icon: Icon, description }: { 
    title: string; 
    gradients: string[]; 
    icon: any; 
    description: string;
  }) => (
    <div className="group relative">
      <div className="mb-4 flex items-center justify-between">
        <div className="flex items-center gap-2">
          <Icon className="h-4 w-4 text-purple/50" />
          <h4 className="text-sm font-semibold text-foreground">{title}</h4>
          <Badge className="text-xs px-2 py-0.5 bg-primary/10 0 text-purple border border-primary/10">
            {gradients?.length}
          </Badge>
        </div>
        <span className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
          {description}
        </span>
      </div>
      <div className="grid grid-cols-7 gap-2">
        {gradients.map((gradient: string, index: number) => (
          <Button
            key={gradient}
            type="button"
            variant="secondary"
            className={cn(
              "aspect-square h-[1.85rem] w-[1.85rem] overflow-hidden rounded-md p-[1px] transition-all duration-300",
              "hover:scale-105 hover:shadow-lg hover:shadow-primary/25 hover:brightness-105",
              " hover:ring-1 hover:ring-primary/20",
              "transform-gpu active:scale-95 active:shadow-inner",
              "group relative cursor-pointer",
              gradient === backgroundInStore && !imageBackground && 
              "ring-2 ring-primary ring-offset-2 ring-offset-background scale-105 shadow-lg shadow-primary/30 brightness-105"
            )}
            onClick={() =>
              handleGradientClick(
                {
                  gradient,
                  background: gradient,
                  type: title === 'Mesh' ? 'Mesh' : 'Normal',
                },
                title === 'Mesh'
              )
            }
            style={{ background: gradient }}
          >
            {gradient === backgroundInStore &&
              !imageBackground &&
              backgroundType !== 'mesh' && (
                <Rocket
                      className="h-4 w-4 text-white drop-shadow-lg transition-all duration-1000 repeat-infinite animate-pulse"
                    />
              )}
          </Button>
        ))}
      </div>
    </div>
  )

  console.log(dominantColor)

  return (
    <div>
      {/* Premium Header */}
      <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">
        <div className="absolute inset-0 bg-gradient-to-r from-primary/5 via-primary/10 to-primary/5" />
        <div className="relative">
          <h4 className="font-medium text-foreground mb-1">Adaptive Gradient</h4>
          <p className="text-sm text-muted-foreground mb-4 max-w-md">
            Generate stunning gradients matched to your image&apos;s color palette. {selectedImage ? (
              <>
                Click on the{' '}
                <button 
                  onClick={extractDominantColor}
                  disabled={!selectedImage || isGenerating}
                  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"
                >
                  Generate
                </button>
                {' '}button to generate.
              </>
            ) : 'Please click on the image layer to generate.'}
          </p>
          
          <div
            className="transition-all duration-300"
          >
            <TooltipProvider>
              <Tooltip delayDuration={100}>
                <TooltipTrigger asChild>
                  <div className="inline-block">
                    <SpotlightButton
                      as={!selectedImage ? 'div' : 'button'}
                      onClick={extractDominantColor}
                      text={isGenerating ? "Analyzing..." : (selectedImage && images[selectedImage - 1]?.linearGradients ? "Regenerate" : "Generate")}
                      disabled={!selectedImage || isGenerating}
                    />
                  </div>
                </TooltipTrigger>
                <TooltipContent
                  side="top"
                  align="start"
                  className="max-w-[10rem] z-[1000] text-center"
                  style={{
                    display: selectedImage ? 'none' : 'block',
                  }}
                >
                  <div className="flex items-center text-left gap-2 mb-2">
                    <span className="font-medium">Image Required</span>

                  </div>
                  <p className="text-sm text-left">
                    Please click on the image layer on canvas first!
                  </p>
                </TooltipContent>
              </Tooltip>
            </TooltipProvider>

                          {isGenerating && (
                <div className="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
                  <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
                  <span>Analyzing color palette...</span>
                </div>
              )}
          </div>
        </div>
      </div>

      {/* Premium Gradient Collections */}
      {selectedImage && images[selectedImage - 1]?.linearGradients && (
        <div className="mt-8 space-y-8">
          <GradientSection
            title="Linear"
            gradients={images[selectedImage - 1]?.linearGradients || []}
            icon={Wand2}
            description="Directional flow"
          />
          
          <GradientSection
            title="Radial"
            gradients={images[selectedImage - 1]?.radialGradients || []}
            icon={Sparkles}
            description="Spotlight & circular"
          />
          
          <GradientSection
            title="Mesh"
            gradients={images[selectedImage - 1]?.meshGradients || []}
            icon={Palette}
            description="Complex multi-layered"
          />

          {/* Premium Tip */}
          <div className="relative overflow-hidden rounded-xl border border-border/50 bg-gradient-to-r from-muted/30 to-muted/50 p-4">
            <div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-primary/10" />
            <div className="relative flex items-start gap-3">
             
              <div>
                <h4 className="font-medium text-foreground mb-1">Pro Tip</h4>
                <p className="text-sm text-muted-foreground">
                  Mesh gradients work beautifully with{' '}
                  <button 
                    onClick={() => setNoise(0.15)}
                    className="font-medium text-purple hover:text-purple/80 underline decoration-purple/50 hover:decoration-purple transition-colors"
                  >
                    15% noise
                  </button>
                  {' '}for that premium mesh effect.
                </p>
              </div>
            </div>
          </div>
        </div>
      )}

      <h3 className="mt-12 flex items-center gap-2 text-xs font-medium uppercase text-muted-foreground">
        <span>Standard Gradients</span>
      </h3>

      <div className="mt-4 flex w-full grid-cols-7 flex-wrap gap-[0.5rem] md:grid">
        {gradients.map(({ gradient, background, type }: Gradient) => (
          <Button
            key={gradient}
            type="button"
            variant="secondary"
            className={`h-[1.85rem] w-[1.85rem] overflow-hidden rounded-md p-[1px] ${
              gradient === backgroundInStore &&
              !imageBackground &&
              'outline-none ring-2 ring-ring ring-offset-2'
            }`}
            onClick={() =>
              handleGradientClick(
                {
                  gradient,
                  background,
                  type: 'Normal',
                },
                type === 'Mesh' // will be true if its of type Mesh
              )
            }
            style={
              type === 'Normal'
                ? { background: gradient }
                : { backgroundColor: background, backgroundImage: gradient }
            }
          >
            {gradient === backgroundInStore &&
              !imageBackground &&
              backgroundType !== 'mesh' && (
                <Popover>
                  <PopoverTrigger asChild>
                    <Settings2 className="flex-center" color="#333" size={20} />
                  </PopoverTrigger>
                  <PopoverContent className="flex w-[12rem] flex-col items-center gap-3">
                    <h1 className="text-[0.85rem]">Gradient angle</h1>
                    <div className={`circular-slider`}>
                      <CircularSliderComp />
                    </div>
                  </PopoverContent>
                </Popover>
              )}
          </Button>
        ))}
      </div>

      <ImageGradientPicker />
    </div>
  )
}


================================================
FILE: components/editor/background-options/pattern-picker.tsx
================================================
'use client'

import { Button } from '@/components/ui/button'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'
import { Skeleton } from '@/components/ui/skeleton'
import { Switch } from '@/components/ui/switch'
import { toast } from '@/hooks/use-toast'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useQuery } from '@tanstack/react-query'
import { Settings2 } from 'lucide-react'
import { Key, useState } from 'react'

export default function PatternPicker() {
  const {
    setImageBackground,
    imageBackground,
    setAttribution,
    setHighResBackground,
    highResBackground,
    setBackgroundType,
  } = useBackgroundOptions()
  const [currentPage, setCurrentPage] = useState(1)

  const fetchUnsplashPatterns = async (page: number) => {
    const response = await fetch(
      `https://api.unsplash.com/collections/W121KJsaTEs/photos?page=${page}&per_page=30&q=100&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`
    )
    const data = await response.json()
    return data
  }

  const {
    isLoading,
    isError,
    data: unsplashData,
    error,
  } = useQuery({
    queryKey: ['unsplash-patterns', currentPage],
    queryFn: () => fetchUnsplashPatterns(currentPage),
  })

  if (isLoading) {
    const skeletonLoaders = Array.from({ length: 30 }).map((_, index) => (
      <li
        className={`h-[2.56rem] w-[2.56rem] rounded-md`}
        key={`skeleton-${index}`}
      >
        <Skeleton className="h-full w-full rounded-md" />
      </li>
    ))

    return (
      <>
        <h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
          <span>Abstract:</span>
        </h3>
        <ul className="mt-4 grid auto-rows-auto grid-cols-4 gap-4 md:max-w-[18rem] md:grid-cols-5">
          {skeletonLoaders}
        </ul>
      </>
    )
  }

  if (isError && error instanceof Error) {
    toast({
      title: 'Error',
      description: error.message,
      variant: 'destructive',
    })
    return <span>Error: {error.message}</span>
  }

  return (
    <>
      <h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
        <span>Abstract:</span>
        <Popover>
          <PopoverTrigger asChild>
            <Settings2 size={20} className="rotate-90" />
          </PopoverTrigger>
          <PopoverContent className="flex w-fit flex-col flex-wrap gap-3">
            <div className="flex gap-3">
              <h1 className="text-[0.85rem]">High resolution background</h1>
              <Switch
                checked={highResBackground}
                onCheckedChange={(checked) => {
                  setHighResBackground(checked)
                }}
              />
            </div>
          </PopoverContent>
        </Popover>
      </h3>

      <ul className="mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid">
        {unsplashData.map(
          (data: {
            user: any
            links: any
            id: Key | null | undefined
            urls: {
              regular: string | null
              small_s3: string | undefined
              full: string | undefined
            }
            alt_description: string | undefined
          }) => (
            <li className={`h-[2.56rem] w-[2.56rem] rounded-md`} key={data.id}>
              <button
                className={`h-full w-full rounded-md ${
                  imageBackground ===
                    (highResBackground
                      ? `${data.urls.full}`
                      : `${data.urls.regular}`) &&
                  'outline-none ring-2 ring-ring ring-offset-2'
                }`}
                onClick={() => {
                  setBackgroundType('pattern')
                  setImageBackground(
                    highResBackground
                      ? `${data.urls.full}`
                      : `${data.urls.regular}`
                  )
                  setAttribution({
                    name: data.user.first_name,
                    link: data.user.username,
                  })
                }}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  className="h-full w-full rounded-md object-cover"
                  src={data.urls.small_s3!}
                  alt={data.alt_description}
                />
              </button>
            </li>
          )
        )}
      </ul>

      <div className="flex justify-end gap-2 md:max-w-[18rem]">
        <Button
          size="sm"
          variant={'stylish'}
          disabled={currentPage === 1}
          className="mt-4 text-sm"
          onClick={() => {
            setCurrentPage((prevPage) => prevPage - 1)
          }}
        >
          &larr; Back
        </Button>
        <Button
          size="sm"
          variant={'stylish'}
          disabled={currentPage === 2}
          className="mt-4 text-sm"
          onClick={() => {
            setCurrentPage((prevPage) => prevPage + 1)
          }}
        >
          Next &rarr;
        </Button>
      </div>
    </>
  )
}


================================================
FILE: components/editor/browser-frames.tsx
================================================
// This component is responsible for rendering the browser frame around the image layer.

'use client'

import { FrameTypes, useFrameOptions } from '@/store/use-frame-options'
import { cn } from '@/utils/button-utils'

const FrameButton = ({ color }: { color: string }) => (
  <div
    className={'rounded-full'}
    style={{
      backgroundColor: color,
    }}
  />
)

const FrameButtons = ({
  hasButtonColor,
  frame,
}: {
  hasButtonColor: boolean
  frame: FrameTypes
}) => {
  const { frameHeight } = useFrameOptions()
  let colors: string[] = []

  if (hasButtonColor && frame === 'MacOS Dark') {
    colors = ['#f7645c', '#fbc341', '#3cc84a']
  } else if (hasButtonColor && frame === 'MacOS Light') {
    colors = ['#f7645ccc', '#fbc341d2', '#3cc84ac5']
  } else if (!hasButtonColor && frame === 'MacOS Dark') {
    colors = ['#ffffff33', '#ffffff33', '#ffffff33']
  } else if (!hasButtonColor && frame === 'MacOS Light') {
    colors = ['#00000033', '#00000033', '#00000033']
  }

  return (
    <div
      className={`mr-2 flex basis-[6%] ${
        frameHeight === 'small'
          ? 'gap-[0.6vw] [&>*]:h-[0.7vw] [&>*]:w-[0.7vw]'
          : frameHeight === 'medium'
          ? 'gap-[0.65vw] [&>*]:h-[0.8vw] [&>*]:w-[0.8vw]'
          : 'gap-[0.7vw] [&>*]:h-[0.9vw] [&>*]:w-[0.9vw]'
      }`}
    >
      {colors.map((color, index) => (
        <FrameButton key={index} color={color} />
      ))}
    </div>
  )
}

const FrameSearchBar = ({ frame }: { frame: FrameTypes }) => (
  <div
    className={cn(
      'flex h-[50%] w-full flex-1 items-center rounded-md px-2 opacity-5',
      frame === 'MacOS Dark' ? 'bg-white' : 'bg-[#000]'
    )}
  />
)

const FrameContainer = ({
  frameHeight,
  children,
  style,
  additionalClasses = '',
}: {
  frameHeight: string
  children: React.ReactNode
  style: React.CSSProperties
  additionalClasses?: string
}) => {
  const heightClass =
    frameHeight === 'small'
      ? 'h-[2.6vw] px-[1.6vw]'
      : frameHeight === 'medium'
      ? 'h-[3vw] px-[1.8vw]'
      : 'h-[3.4vw] px-[2vw]'

  return (
    <div
      style={style}
      className={`flex items-center gap-4 ${heightClass} ${additionalClasses}`}
    >
      {children}
    </div>
  )
}

export default function BrowserFrame({ frame }: { frame: FrameTypes }) {
  const {
    frameHeight,
    showSearchBar,
    macOsDarkColor,
    macOsLightColor,
    hideButtons,
    hasButtonColor,
  } = useFrameOptions()

  const props = { frame }

  const frameComponents = {
    'MacOS Dark': (
      <FrameContainer
        style={{ background: macOsDarkColor }}
        frameHeight={frameHeight}
      >
        {!hideButtons && (
          <FrameButtons hasButtonColor={hasButtonColor} {...props} />
        )}
        {showSearchBar && <FrameSearchBar {...props} />}
      </FrameContainer>
    ),
    'MacOS Light': (
      <FrameContainer
        style={{ background: macOsLightColor }}
        frameHeight={frameHeight}
        additionalClasses="border-b border-[#00000010]"
      >
        {!hideButtons && (
          <FrameButtons hasButtonColor={hasButtonColor} {...props} />
        )}
        {showSearchBar && <FrameSearchBar {...props} />}
      </FrameContainer>
    ),
    None: null,
    Arc: null,
    Shadow: null,
  }

  return frameComponents[frame] || null
}


================================================
FILE: components/editor/canvas-area.tsx
================================================
'use client'

import useAutomaticAspectRatioSwitcher from '@/hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher'
import useCanvasResizeObserver from '@/hooks/canvas-area-hooks/use-resize-observer'
import useScreenSizeWarningToast from '@/hooks/canvas-area-hooks/use-screen-size-warning-toast'
import { useEventListener } from '@/hooks/use-event-listener'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import dynamic from 'next/dynamic'
import React, { CSSProperties, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import BackgroundImageCanvas from './background-image-canvas'
import ImageUpload from './main-image-area'
import MobileViewImageOptions from './mobile-view-image-options'
import SelectoComponent from './selecto-component'
import TextLayers from './text-layers'
import TiptapMoveable from './tiptap-moveable'

const MoveableComponent = dynamic(
  () => import('./moveable-component').then((mod) => mod.default),
  { ssr: false }
)

export default function Canvas() {
  const { backgroundType } = useBackgroundOptions()
  const {
    resolution,
    exactDomResolution,
    scrollScale,
    setScrollScale,
    canvasRoundness,
  } = useResizeCanvas()
  const { scale } = useImageOptions()
  const {
    selectedImage,
    selectedText,
    setSelectedImage,
    enableCrop,
    setSelectedText,
  } = useSelectedLayers()
  const screenshotRef = useRef<HTMLDivElement | null>(null)
  const parentRef = useRef<HTMLDivElement | null>(null)
  const {
    showControls,
    setShowControls,
    setShowTextControls,
    setIsMultipleTargetSelected,
    showTextControls,
    isEditable,
    setIsEditable,
    isSelecting,
  } = useMoveable()

  const [width, height]: number[] = resolution.split('x').map(Number)

  const aspectRatio = width / height

  console.log(`Current DOM resoltion: ${exactDomResolution}`)

  let style: CSSProperties = {
    aspectRatio,
    backgroundImage: `var(--gradient-bg)`,
    borderRadius: `${canvasRoundness * 16}px`,
  }

  if (backgroundType === 'mesh') {
    style = {
      ...style,
      backgroundColor: `var(--mesh-bg)`,
    }
  }

  if (backgroundType === 'solid') {
    style = {
      ...style,
      backgroundColor: `var(--solid-bg)`,
    }
  }

  // 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.
  useCanvasResizeObserver(screenshotRef)

  // 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.
  useAutomaticAspectRatioSwitcher({
    containerRef: parentRef,
    screenshotRef,
  })

  // This hook shows a warning toast if the screen size is less than 768px.
  useScreenSizeWarningToast()

  const handleScroll = (e: React.WheelEvent<HTMLDivElement>) => {
    if (typeof window !== 'undefined' && window.innerWidth <= 768) {
      return
    }
    if (enableCrop) return
    if (e.deltaY < 0) {
      // Scrolling up
      if (scrollScale === 1) return
      setScrollScale(scrollScale + 0.1) // Increment the scroll scale by 0.1
    } else if (e.deltaY > 0) {
      // Scrolling down
      if (scrollScale <= 0.4) return
      setScrollScale(scrollScale - 0.1) // Decrement the scroll scale by 0.1
    }
  }

  let parentScaleStyle = {
    scale: `${scrollScale}`,
  }

  // Close the image controls when the escape key is pressed
  useHotkeys('Escape', () => {
    if (showControls) {
      setShowControls(false)
      setIsMultipleTargetSelected(false)
    }
  })

  // 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.
  useEventListener(
    'click',
    (e: any) => {
      const isCanvasArea =
        e?.target?.classList?.contains('canvas-container') ||
        e?.target?.classList?.contains('selecto-area')

      if (isCanvasArea) {
        setSelectedText(null)
        setShowTextControls(false)
        setShowTextControls(false)
        setIsEditable(false)
      }

      if (isSelecting || (!selectedImage && !showControls)) return
      if (isCanvasArea) {
        setSelectedImage(null)
        setShowTextControls(false)
        setShowControls(false)
        setIsMultipleTargetSelected(false)
      }
    },
    screenshotRef
  )

  return (
    <>
      <section
        ref={parentRef}
        className={`relative flex h-full w-full flex-col overflow-hidden bg-[#111] md:grid md:place-items-center ${
          aspectRatio <= 1 ? 'p-4 md:p-8' : 'p-4 md:p-8'
        }
        `}
        style={parentScaleStyle}
        onWheel={handleScroll}
      >
        <div
          className={`canvas-container relative flex items-center justify-center overflow-hidden ${
            aspectRatio <= 1
              ? 'h-auto w-full lg:h-full lg:w-auto'
              : 'h-auto w-full'
          }`}
          ref={screenshotRef}
          id="canvas-container"
          style={style}
        >
          <BackgroundImageCanvas />
          {showControls && <MoveableComponent id={`${selectedImage}`} />}
          {showTextControls && !isEditable && (
            <TiptapMoveable id={`text-${selectedText}`} />
          )}

          <div
            className="selecto-area relative flex h-full w-full place-items-center items-center justify-center"
            style={{
              scale,
            }}
          >
            <ImageUpload />
            <TextLayers />
          </div>
        </div>
        <MobileViewImageOptions />
      </section>

      <SelectoComponent />
    </>
  )
}


================================================
FILE: components/editor/canvas-options/canvas-roundness-slider.tsx
================================================
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import { RotateCcw } from 'lucide-react'

export default function CanvasRoundnessSlider() {
  const { canvasRoundness, setCanvasRoundness } = useResizeCanvas()

  return (
    <>
      <div className="mb-3 mt-4 flex items-center px-1 md:max-w-full">
        <h1 className="text-[0.85rem]">Roundness</h1>
        <p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
          {`${Math.round((canvasRoundness / 3) * 100)} `}
        </p>
        <Button
          aria-label="reset roundness"
          variant="secondary"
          size="sm"
          className="ml-auto translate-x-2"
          onClick={() => setCanvasRoundness(0)}
        >
          <RotateCcw size={15} className="text-dark/80" />
        </Button>
      </div>
      <div className="flex gap-4 text-[0.85rem] md:max-w-full">
        <Slider
          defaultValue={[0]}
          max={3}
          min={0}
          step={0.01}
          value={[canvasRoundness]}
          onValueChange={(value: number[]) => {
            setCanvasRoundness(value[0])
          }}
          onDecrement={() => {
            if (canvasRoundness <= 0) return
            setCanvasRoundness(canvasRoundness - 0.03)
          }}
          onIncrement={() => {
            if (canvasRoundness >= 3) return
            setCanvasRoundness(canvasRoundness + 0.03)
          }}
        />
      </div>
    </>
  )
}


================================================
FILE: components/editor/canvas-options/index.tsx
================================================
'use client'

import {
  Dribbble,
  Facebook,
  Instagram,
  Linkedin,
  Plus,
  Minus,
  Twitter,
  Youtube,
  ArrowRight,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { resolutions } from '@/utils/presets/resolutions'
import { Button } from '@/components/ui/button'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import { Separator } from '@/components/ui/separator'
import { ResolutionButton } from './resolution-button'
import CanvasRoundnessSlider from './canvas-roundness-slider'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import {
  TooltipProvider,
  Tooltip,
  TooltipTrigger,
  TooltipContent,
} from '@/components/ui/tooltip'
import Icon from '@/components/icons'

const icons = {
  Youtube: <Youtube size={18} />,
  Instagram: <Instagram size={18} />,
  Facebook: <Facebook size={18} />,
  LinkedIn: <Linkedin size={18} />,
  Twitter: <Twitter size={18} />,
  Dribble: <Dribbble size={18} />,
  ProductHunt: (
    <div className="flex-center h-6 w-6 rounded-full bg-[#898aeb]/5">P</div>
  ),
}

const splitResolution = (resolution: string) => resolution.split('x')

export default function CanvasOptions() {
  const {
    setResolution,
    domResolution,
    scrollScale,
    setScrollScale,
    automaticResolution,
    setAutomaticResolution,
  } = useResizeCanvas()

  const [width, height] = splitResolution(domResolution)

  const [inputResolution, setInputResolution] = useState({
    inputWidth: width,
    inputHeight: height,
  })

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    setResolution(
      `${inputResolution.inputWidth}x${inputResolution.inputHeight}`
    )
  }

  useEffect(() => {
    setInputResolution({
      inputWidth: `${Math.round(+width)}`,
      inputHeight: `${Math.round(+height)}`,
    })
  }, [height, width])

  return (
    <>
      <div className="mt-4 flex w-full justify-between px-1">
        <div className="flex-center">
          <h1 className="mr-1 text-[0.85rem]">Auto resolution</h1>
          <TooltipProvider>
            <Tooltip delayDuration={100}>
              <TooltipTrigger>
                <Icon variant="duotone" name="info" color="none" />
              </TooltipTrigger>
              <TooltipContent className="max-w-[12rem]">
                <p>
                  When enabled, the canvas will automatically resize to fit your
                  image when you upload it.
                </p>
              </TooltipContent>
            </Tooltip>
          </TooltipProvider>
        </div>
        <Switch
          checked={automaticResolution}
          onCheckedChange={(checked) => {
            setAutomaticResolution(checked)
          }}
        />
      </div>

      <hr className="my-4 border-border" />

      <h1 className="mb-3 mt-8 px-1 text-[0.85rem]">Custom Resolution</h1>
      <form
        onSubmit={handleSubmit}
        className="flex w-full max-w-sm items-center space-x-2"
      >
        <Input
          type="number"
          value={inputResolution.inputWidth}
          min={100}
          max={5000}
          onChange={(e) => {
            setInputResolution({
              ...inputResolution,
              inputWidth: e.target.value,
            })
          }}
          className="rounded-lg text-sm"
        />
        <span className="mx-2 my-auto">x</span>
        <Input
          type="number"
          value={inputResolution.inputHeight}
          min={100}
          max={5000}
          className="rounded-lg text-sm"
          onChange={(e) => {
            setInputResolution({
              ...inputResolution,
              inputHeight: e.target.value,
            })
          }}
        />
        <Button
          type="submit"
          variant="outline"
          className="rounded-lg px-3 text-sm"
        >
          <ArrowRight size={18} />
        </Button>
      </form>

      <h1 className="mb-3 mt-8 px-1 text-[0.85rem]">Resolutions</h1>
      <div className="flex flex-wrap gap-3">
        {resolutions.map((res, index) => (
          <ResolutionButton
            key={index}
            resolutions={res?.resolutions}
            name={res?.name}
            icon={icons[res?.icon as keyof typeof icons]}
            // variant={res.resolutions === resolution ? 'stylish' : 'outline'}
            color={res.color}
            variant="stylish"
            className="rounded-lg"
          />
        ))}
      </div>
      <Separator className="mt-8 h-[0.1rem] w-full" />

      <CanvasRoundnessSlider />

      <Separator className="mt-8 h-[0.1rem] w-full" />

      <h1 className="mb-3 mt-4 px-1 text-[0.85rem]">Preview scale</h1>
      <span className="inline-flex rounded-md shadow-sm">
        <button
          type="button"
          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"
          disabled={scrollScale === 1}
          onClick={() => {
            if (scrollScale === 1) return
            setScrollScale(scrollScale + 0.1)
          }}
        >
          <span className="sr-only">Scale up</span>
          <Plus className="h-5 w-5" aria-hidden="true" />
        </button>
        <button
          type="button"
          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"
          disabled={scrollScale <= 0.4}
          onClick={() => {
            if (scrollScale <= 0.4) return
            setScrollScale(scrollScale - 0.1)
          }}
        >
          <span className="sr-only">Scale down</span>
          <Minus className="h-5 w-5" aria-hidden="true" />
        </button>
      </span>
    </>
  )
}


================================================
FILE: components/editor/canvas-options/resolution-button.tsx
================================================
import { useState } from 'react'
import { cn } from '@/utils/button-utils'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import { Button } from '@/components/ui/button'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'
import { PopoverArrow } from '@radix-ui/react-popover'
import { ChevronDown } from 'lucide-react'
import { calculateEqualCanvasSize } from '@/utils/helper-fns'

export function ResolutionButton({
  resolutions,
  name,
  icon,
  className,
  color,
  variant,
}: {
  resolutions: any
  name: string
  icon?: React.ReactNode
  className?: string
  color?: string
  variant: 'outline' | 'stylish'
}) {
  const [isHovering, setIsHovering] = useState(false)

  const { setResolution, setScaleFactor, domResolution } = useResizeCanvas()
  const { images, setImages, initialImageUploaded } = useImageOptions()
  const { selectedImage } = useSelectedLayers()

  const [domWidth]: number[] = domResolution.split('x').map(Number)

  const handleMouseOver = () => {
    setIsHovering(true)
  }

  const handleMouseOut = () => {
    setIsHovering(false)
  }

  const calculateCanvasSize = (
    imgWidth: number,
    imgHeight: number,
    padding: number
  ) => {
    const aspectRatio = imgWidth / imgHeight
    let canvasWidth, canvasHeight

    if (aspectRatio > 1) {
      canvasWidth = imgWidth + 2 * padding
      canvasHeight = canvasWidth / aspectRatio
    } else {
      canvasHeight = imgHeight + 2 * padding
      canvasWidth = canvasHeight * aspectRatio
    }

    return `${canvasWidth}x${canvasHeight}`
  }

  if (name === 'Fit') {
    return (
      <Button
        name="Fit"
        size="sm"
        className={cn('flex items-center gap-2 rounded-lg', className)}
        variant={variant}
        onClick={() => {
          if (images.length === 0) return

          const padding = 200
          const img = new Image()
          img.src = images[0].image

          img.onload = () => {
            const { naturalWidth, naturalHeight } = img
            const newResolution = calculateCanvasSize(
              naturalWidth,
              naturalHeight,
              padding
            )
            setResolution(newResolution.toString())
            selectedImage &&
              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          imageSize: '0.75',
                        },
                      }
                    : image
                )
              )
          }
        }}
        aria-label={name}
      >
        Fit image
      </Button>
    )
  }

  if (name === 'Equal padding') {
    return (
      <Button
        name="Equal padding"
        size="sm"
        className={cn('flex items-center gap-2 rounded-lg', className)}
        variant={variant}
        onClick={() => {
          if (images.length === 0) return

          const padding = 250
          const img = new Image()
          img.src = images[0].image

          img.onload = () => {
            const { naturalWidth, naturalHeight } = img
            const newResolution = calculateEqualCanvasSize(
              naturalWidth,
              naturalHeight,
              padding
            )
            setResolution(newResolution.toString())
            selectedImage &&
              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          imageSize: '0.75',
                        },
                      }
                    : image
                )
              )
          }
        }}
        aria-label={name}
      >
        Equal padding
      </Button>
    )
  }

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          onMouseOver={handleMouseOver}
          onMouseOut={handleMouseOut}
          className={cn(
            'flex items-center gap-1.5 rounded-lg transition-colors',
            className
          )}
          style={{
            backgroundColor: isHovering ? `${color}1A` : '',
            color: isHovering ? `${color}` : '',
            borderColor: isHovering ? `${color}33` : '',
          }}
          variant={variant}
          size="sm"
        >
          {icon && <div>{icon}</div>}
          <div className="sr-only">{name}</div>
          <ChevronDown size={18} className="translate-y-[1.5px] text-inherit" />
        </Button>
      </PopoverTrigger>
      <PopoverContent
        avoidCollisions
        className="grid w-[220px] grid-cols-1 gap-3"
      >
        <PopoverArrow
          width={14}
          height={7}
          className="stroke fill-[#1A1C1F] stroke-border"
        />
        {resolutions?.map((res: { resolution: string; preset: string }) => {
          return (
            <Button
              onClick={() => {
                const [outputWidth]: number[] = res.resolution
                  .split('x')
                  .map(Number)

                if (!initialImageUploaded) return

                setResolution(res.resolution)

                setScaleFactor(outputWidth / domWidth)
              }}
              variant="stylish"
              size={'sm'}
              style={{
                backgroundColor: `${color}1A`,
                color: `${color}`,
                borderColor: `${color}33`,
              }}
              key={res.resolution}
            >
              <p>{`${res.preset}`}</p>
              &nbsp;
              <p>({res.resolution})</p>
            </Button>
          )
        })}
      </PopoverContent>
    </Popover>
  )
}


================================================
FILE: components/editor/frame-options/additional-frame-options.tsx
================================================
import PopupColorPicker from '@/components/popup-color-picker'
import { Switch } from '@/components/ui/switch'
import { useFrameOptions } from '@/store/use-frame-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { Settings2 } from 'lucide-react'

export default function AdditionalFrameOptions() {
  const {
    setShowSearchBar,
    setShowStroke,
    macOsDarkColor,
    setMacOsDarkColor,
    setMacOsLightColor,
    macOsLightColor,
    setArcDarkMode,
    showStroke,
    arcDarkMode,
    hideButtons,
    setHideButtons,
    hasButtonColor,
    setHasButtonColor,
  } = useFrameOptions()
  const { selectedImage } = useSelectedLayers()
  const { images } = useImageOptions()

  const browserFrame = selectedImage ? images[selectedImage - 1]?.frame : 'None'

  if (browserFrame !== 'None')
    return (
      <div
        className={`${selectedImage ? '' : 'pointer-events-none opacity-40'} `}
      >
        <h3 className="mb-6 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
          <Settings2 size={20} />
          <span>Additional options</span>
        </h3>

        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
          <div className="mb-6 flex items-center justify-between gap-4 px-1">
            <h1 className="text-[0.85rem]">Show searchbar :</h1>
            <Switch
              defaultChecked={false}
              onCheckedChange={(checked) => {
                setShowSearchBar(checked)
              }}
            />
          </div>
        )}

        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
          <div className="mb-6 flex items-center justify-between gap-4 px-1">
            <h1 className="text-[0.85rem]">Colorful buttons :</h1>
            <Switch
              defaultChecked={true}
              checked={hasButtonColor}
              onCheckedChange={(checked) => {
                setHasButtonColor(checked)
              }}
            />
          </div>
        )}

        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
          <div className="mb-6 flex items-center justify-between gap-4 px-1">
            <h1 className="text-[0.85rem]">Hide buttons :</h1>
            <Switch
              defaultChecked={false}
              checked={hideButtons}
              onCheckedChange={(checked) => {
                setHideButtons(checked)
              }}
            />
          </div>
        )}

        {(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
          <div className="mb-6 flex items-center justify-between gap-4 px-1">
            <h1 className="text-[0.85rem]">Frame color :</h1>
            <PopupColorPicker
              shouldShowDropdown={false}
              shouldShowAlpha={true}
              color={
                browserFrame === 'MacOS Dark' ? macOsDarkColor : macOsLightColor
              }
              onChange={(color) => {
                if (browserFrame === 'MacOS Dark') {
                  setMacOsDarkColor(color)
                } else {
                  setMacOsLightColor(color)
                }
              }}
            />
          </div>
        )}

        {browserFrame === 'Shadow' && (
          <div className="mb-6 flex items-center gap-4 px-1 md:max-w-full">
            <h1 className="text-[0.85rem]">Show outline :</h1>
            <Switch
              defaultChecked={true}
              checked={showStroke}
              onCheckedChange={(checked) => {
                setShowStroke(checked)
              }}
            />
          </div>
        )}

        {browserFrame === 'Arc' && (
          <div className="mb-6 flex items-center gap-4 px-1 md:max-w-full">
            <h1 className="text-[0.85rem]">Dark mode :</h1>
            <Switch
              defaultChecked={false}
              checked={arcDarkMode}
              onCheckedChange={(checked) => {
                setArcDarkMode(checked)
              }}
            />
          </div>
        )}
      </div>
    )
}


================================================
FILE: components/editor/frame-options/frame-picker.tsx
================================================
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { FrameTypes, useFrameOptions } from '@/store/use-frame-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import { cn } from '@/utils/button-utils'

export default function FramePicker() {
  const { setFrameHeight, frameHeight } = useFrameOptions()
  const { selectedImage } = useSelectedLayers()
  const { setImages, images } = useImageOptions()
  const { setShowControls } = useMoveable()

  const frameChangeHandler = (frame: FrameTypes) => {
    selectedImage &&
      setImages(
        images.map((image, index) =>
          index === selectedImage - 1
            ? {
                ...image,
                frame,
                style: {
                  ...image.style,
                  imageRoundness:
                    frame === 'None' ? 0.4 : frame === 'Arc' ? 1.5 : 0.7,
                },
              }
            : image
        )
      )
    setShowControls(false)
  }

  return (
    <>
      <div
        className={`mb-3 mt-4 flex items-center px-1 md:max-w-full 
        ${selectedImage ? '' : 'pointer-events-none opacity-40'}
      `}
      >
        <h1 className="text-[0.85rem]">Browser frames:</h1>
      </div>

      <div className="mt-2 grid w-full grid-cols-3 flex-wrap gap-x-2.5 gap-y-6">
        <FrameContainer
          text="None"
          onClick={() => {
            frameChangeHandler('None')
          }}
        >
          <div className="flex h-full w-full flex-col justify-center overflow-hidden rounded-sm">
            <div className="w-full flex-1 bg-primary/80" />
          </div>
        </FrameContainer>

        <FrameContainer
          text="MacOS Dark"
          onClick={() => {
            frameChangeHandler('MacOS Dark')
          }}
        >
          <div className="flex h-full w-full flex-col justify-center overflow-hidden rounded-sm">
            <div className="flex w-full basis-[30%] bg-[#454545] shadow-sm">
              <div className={`flex-center basis-[50%] gap-0.5 `}>
                <div className="h-1 w-1 rounded-full bg-[#f7645ccc]" />
                <div className="h-1 w-1 rounded-full bg-[#fbc341d2]" />
                <div className="h-1 w-1 rounded-full bg-[#3cc84ac5]" />
              </div>
            </div>
            <div className="w-full flex-1 bg-primary/80" />
          </div>
        </FrameContainer>

        <FrameContainer
          text="MacOS Light"
          onClick={() => {
            frameChangeHandler('MacOS Light')
          }}
        >
          <div className="flex h-full w-full flex-col justify-center overflow-hidden rounded-sm">
            <div className="flex w-full basis-[30%] bg-[#E3E2E3] shadow-sm">
              <div className={`flex-center basis-[50%] gap-0.5 `}>
                <div className="h-1 w-1 rounded-full bg-[#f7645ccc]" />
                <div className="h-1 w-1 rounded-full bg-[#fbc341d2]" />
                <div className="h-1 w-1 rounded-full bg-[#3cc84ac5]" />
              </div>
            </div>
            <div className="w-full flex-1 bg-primary/80" />
          </div>
        </FrameContainer>

        <FrameContainer
          text="Arc"
          onClick={() => {
            frameChangeHandler('Arc')
          }}
        >
          <div className="flex-center h-[4.5rem] w-24 flex-col rounded-sm border border-[#fff]/20 bg-[#fff]/20 p-1 shadow-xl">
            <div className="h-full w-full rounded-[2px] bg-primary shadow-sm" />
          </div>
        </FrameContainer>

        <FrameContainer
          text="Shadow"
          onClick={() => {
            frameChangeHandler('Shadow')
          }}
          className="translate-y-2"
        >
          <div className="flex-center h-[4.5rem] w-24 flex-col rounded-sm">
            <div className="h-full w-full rounded-[2px] bg-primary/80" />
          </div>
        </FrameContainer>
      </div>

      {selectedImage &&
        images[selectedImage - 1]?.frame !== 'Shadow' &&
        images[selectedImage - 1]?.frame !== 'None' && (
          <div
            className={`mt-8 flex flex-col gap-3 px-1 md:max-w-full ${
              selectedImage ? '' : 'pointer-events-none opacity-40'
            }`}
          >
            <h1 className="text-[0.85rem]">Frame size</h1>
            <Select
              defaultValue={frameHeight}
              onValueChange={(value) => setFrameHeight(value)}
            >
              <SelectTrigger className="w-[7rem]">
                <SelectValue placeholder="Medium" />
              </SelectTrigger>
              <SelectContent className="w-[7rem]">
                <SelectItem value="small">Small</SelectItem>
                <SelectItem value="medium">Medium</SelectItem>
                <SelectItem value="large">Large</SelectItem>
              </SelectContent>
            </Select>
          </div>
        )}
    </>
  )
}

export function FrameContainer({
  children,
  text,
  onClick,
  className,
}: {
  children: React.ReactNode
  text: FrameTypes
  onClick?: () => void
  className?: string
}) {
  const { selectedImage } = useSelectedLayers()
  const { images } = useImageOptions()

  return (
    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
      <button
        onClick={onClick}
        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 ${
          selectedImage && images[selectedImage - 1]?.frame === text
            ? 'ring-2 ring-ring ring-offset-2'
            : ''
        }`}
      >
        <div
          className={cn(
            'absolute bottom-0 h-2/3 w-2/3 translate-x-1/2 translate-y-1 scale-150  overflow-hidden rounded-sm',
            className
          )}
          style={{
            boxShadow: `0px 10px 40px #000${
              text === 'Shadow' ? ',-4px -3.5px rgba(0,0,0,0.8)' : ''
            }`,
          }}
        >
          {children}
        </div>
      </button>

      <p className="mt-2 text-center text-[0.75rem] font-medium text-dark">
        {text.replace('MacOS', 'Mac')}
      </p>
    </div>
  )
}


================================================
FILE: components/editor/frame-options/index.tsx
================================================
import RoundnessOption from '../image-options/roundness-option'
import FramePicker from './frame-picker'
import AdditionalFrameOptions from './additional-frame-options'
import { Separator } from '@/components/ui/separator'

export default function FrameOptions() {
  return (
    <>
      <FramePicker />

      <RoundnessOption />

      <Separator className="mt-8 h-[0.1rem] w-full" />

      <AdditionalFrameOptions />
    </>
  )
}


================================================
FILE: components/editor/image-context-menu.tsx
================================================
import { Button } from '@/components/ui/button'
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from '@/components/ui/context-menu'
import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog'
import { useColorExtractor } from '@/store/use-color-extractor'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import {
  BringToFront,
  CropIcon,
  ImagePlus,
  SendToBack,
  Trash,
  Wand,
} from 'lucide-react'
import dynamic from 'next/dynamic'
import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { type Crop } from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
import Loader from '../loader'

const DynamicCropComponent = dynamic(() =>
  import('react-image-crop').then((mod) => mod.ReactCrop)
)

export default function ContextMenuImage({
  children,
}: {
  children: React.ReactNode
}) {
  const [crop, setCrop] = useState<Crop>({
    unit: '%', // Can be 'px' or '%'
    x: 25,
    y: 25,
    width: 50,
    height: 50,
  })
  const imgRef = useRef<HTMLImageElement>(null)
  const { setImages, images } = useImageOptions()
  const [isRemovingBackground, setIsRemovingBackground] = useState(false)
  const [isBgRemovalDialogOpen, setIsBgRemovalDialogOpen] = useState(false)
  const [isProcessingBackground, setIsProcessingBackground] = useState(false)
  const [bgRemovalError, setBgRemovalError] = useState<string | null>(null)
  const [processedImageUrl, setProcessedImageUrl] = useState<string | null>(
    null
  )
  const { selectedImage, setSelectedImage, setEnableCrop, enableCrop } =
    useSelectedLayers()
  const { showControls, setShowControls } = useMoveable()
  const workerRef = useRef<Worker | null>(null)

  const handleImageDelete = (id: number) => {
    if (images.length === 1) {
      setImages([])
      return
    }

    if (selectedImage) {
      setImages(
        images.map((image, index) =>
          index === selectedImage - 1
            ? {
                ...image,
                image: '',
              }
            : image
        )
      )
    }

    setSelectedImage(null)
  }

  const bringToFrontOrBack = (direction: 'front' | 'back') => {
    if (selectedImage) {
      setImages(
        images.map((image, index) =>
          index === selectedImage - 1
            ? {
                ...image,
                style: {
                  ...image.style,
                  zIndex:
                    direction === 'front'
                      ? image.style.zIndex + 1
                      : image.style.zIndex - 1,
                },
              }
            : image
        )
      )
    }
  }

  useEffect(() => {
    // Terminate existing worker if component re-renders or dependencies change
    workerRef.current?.terminate()

    if (isBgRemovalDialogOpen && selectedImage) {
      const currentImage = images[selectedImage - 1]
      if (!currentImage || !currentImage.image) return

      setProcessedImageUrl(null) // Reset image URL first
      setBgRemovalError(null) // Reset error
      setIsProcessingBackground(true)

      // Create and configure the worker
      workerRef.current = new Worker(
        new URL('@/workers/background-removal.worker.ts', import.meta.url)
      )

      workerRef.current.onmessage = (
        event: MessageEvent<
          { type: 'success'; url: string } | { type: 'error'; error: string }
        >
      ) => {
        if (event.data.type === 'success') {
          setProcessedImageUrl(event.data.url)
          setBgRemovalError(null)
        } else if (event.data.type === 'error') {
          console.error('Background removal worker error:', event.data.error)
          setBgRemovalError(event.data.error)
          // Keep dialog open to show error or close?
          // setIsBgRemovalDialogOpen(false);
        }
        setIsProcessingBackground(false)
      }

      workerRef.current.onerror = (error) => {
        console.error('Unhandled worker error:', error)
        setBgRemovalError('An unexpected worker error occurred.')
        setIsProcessingBackground(false)
        // setIsBgRemovalDialogOpen(false);
      }

      // Send image source to worker
      workerRef.current.postMessage({ src: currentImage.image })
    } else {
      // If dialog is closed, ensure worker is terminated
      workerRef.current?.terminate()
      workerRef.current = null
    }

    // Cleanup function to terminate worker on unmount or when dialog closes
    return () => {
      workerRef.current?.terminate()
      workerRef.current = null
    }
    // Rerun effect when dialog opens/closes or selected image changes
  }, [isBgRemovalDialogOpen, selectedImage, images]) // Added images dependency

  useHotkeys(['Delete', 'Backspace'], () => {
    if (selectedImage)
      if (showControls) {
        handleImageDelete(selectedImage)
        setShowControls(false)
        setSelectedImage(null)
      }
  })

  const cropImageNow = () => {
    const canvas = document.createElement('canvas')
    const image = imgRef.current
    if (!image) return
    const scaleX = image.naturalWidth / image.width
    const scaleY = image.naturalHeight / image.height
    canvas.width = crop.width
    canvas.height = crop.height
    const ctx: any = canvas.getContext('2d')

    const pixelRatio = window.devicePixelRatio
    canvas.width = crop.width * pixelRatio * scaleX
    canvas.height = crop.height * pixelRatio * scaleY
    ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)
    ctx.imageSmoothingQuality = 'high'

    ctx.drawImage(
      image,
      crop.x * scaleX,
      crop.y * scaleY,
      crop.width * scaleX,
      crop.height * scaleY,
      0,
      0,
      crop.width * scaleX,
      crop.height * scaleY
    )

    const base64Image = canvas.toDataURL('image/png')
    selectedImage &&
      setImages(
        images.map((image, index) =>
          index === selectedImage - 1
            ? {
                ...image,
                image: base64Image,
              }
            : image
        )
      )
  }

  return (
    <Dialog
      open={enableCrop}
      onOpenChange={(open) => {
        if (open === false) setEnableCrop(false)
        setEnableCrop(open)
      }}
    >
      <ContextMenu>
        <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
        <ContextMenuContent className="w-64">
          <ContextMenuItem
            inset
            onClick={() => {
              bringToFrontOrBack('back')
            }}
            // disabled={
            //   !selectedImage || images[selectedImage - 1].style.zIndex === 2
            // }
          >
            Send back
            <ContextMenuShortcut>
              <BringToFront size={19} className="opacity-80" />
            </ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem
            inset
            onClick={() => {
              bringToFrontOrBack('front')
            }}
          >
            Bring forward
            <ContextMenuShortcut>
              <SendToBack size={19} className="opacity-80" />
            </ContextMenuShortcut>
          </ContextMenuItem>

          <ContextMenuSeparator />

          <ReplaceImage />

          <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">
            <div
              onClick={() => setIsBgRemovalDialogOpen(true)}
              className="ml-6 cursor-pointer"
            >
              Remove background
            </div>
            <span className="ml-auto text-xs tracking-widest text-muted-foreground">
              <Wand size={19} className="opacity-80" />
            </span>
          </div>

          <DialogTrigger asChild>
            <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">
              <div className="ml-6 cursor-pointer">Crop</div>
              <span className="ml-auto text-xs tracking-widest text-muted-foreground">
                <CropIcon size={19} className="opacity-80" />
              </span>
            </div>
          </DialogTrigger>

          {/* <ContextMenuSub>
            <ContextMenuSubTrigger inset>More Tools</ContextMenuSubTrigger>
            <ContextMenuSubContent className="w-48">
            </ContextMenuSubContent>
          </ContextMenuSub> */}

          <ContextMenuSeparator />
          <ContextMenuItem
            inset
            onClick={() => {
              selectedImage && handleImageDelete(selectedImage)
            }}
            className="text-[#F46567]/70 focus:text-[#f46567]/80"
          >
            Delete
            <ContextMenuShortcut>
              <Trash size={19} className="text-[#F46567]/70 opacity-80" />
            </ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
      <DialogContent className="flex h-fit max-h-[95vh]  w-1/2 flex-col gap-4">
        <DialogHeader className="mb-4">
          <DialogTitle>Crop image</DialogTitle>
        </DialogHeader>

        <div className="mb-4 h-full w-full flex-1 overflow-hidden overflow-y-auto">
          {selectedImage && (
            <DynamicCropComponent
              crop={crop}
              onChange={(c) => setCrop(c)}
              disabled={!enableCrop || !selectedImage}
              onComplete={(c) => {
                console.log(c)
              }}
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                ref={imgRef}
                src={images[selectedImage - 1].image}
                alt="Crop selected image"
                className="h-full w-full object-cover"
              />
            </DynamicCropComponent>
          )}
        </div>

        <DialogFooter className="mt-auto flex items-center gap-1.5">
          <Button
            variant="outline"
            onClick={() => {
              setEnableCrop(false)
            }}
          >
            Cancel
          </Button>

          <Button
            onClick={() => {
              setEnableCrop(false)
              cropImageNow()
            }}
            className="flex-center gap-1.5"
          >
            <span>Done</span>
            <CropIcon size={19} className="opacity-80" />
          </Button>
        </DialogFooter>
      </DialogContent>

      <Dialog
        open={isBgRemovalDialogOpen}
        onOpenChange={setIsBgRemovalDialogOpen}
      >
        <DialogContent className="flex h-fit max-h-[95vh] w-1/2 flex-col gap-4">
          <DialogHeader>
            <DialogTitle className="mb-4 flex items-center gap-1.5">
              <Wand size={19} className="opacity-80" />
              <span>Remove Background</span>
            </DialogTitle>
          </DialogHeader>

          <div
            className="relative mb-4 flex h-full w-full flex-1 items-center justify-center overflow-hidden overflow-y-auto"
            style={{
              // Apply checkered background only when done processing, successful, and image is ready
              backgroundImage:
                !isProcessingBackground && processedImageUrl && !bgRemovalError
                  ? '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%)'
                  : 'none',
              backgroundSize:
                !isProcessingBackground && processedImageUrl && !bgRemovalError
                  ? '20px 20px'
                  : 'auto',
              backgroundPosition:
                !isProcessingBackground && processedImageUrl && !bgRemovalError
                  ? '0 0, 0 10px, 10px -10px, -10px 0px'
                  : 'initial',
              // Keep a fallback bg or use theme background
              backgroundColor:
                !isProcessingBackground && processedImageUrl && !bgRemovalError
                  ? 'hsl(var(--background))' // Use theme background
                  : 'hsl(var(--muted) / 0.1)', // Original dim background
            }}
          >
            {isProcessingBackground && (
              <div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-black/60 ">
                <Loader />
              </div>
            )}
            {/* Display Error Message */}
            {!isProcessingBackground && bgRemovalError && (
              <div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-destructive/60 p-4 text-center">
                <p className="font-semibold text-white">
                  Error Removing Background
                </p>
                <p className="mt-1 text-xs text-white/80">{bgRemovalError}</p>
              </div>
            )}
            {selectedImage && !processedImageUrl && (
              <img
                src={images[selectedImage - 1]?.image}
                alt="Original image"
                className="h-full w-full object-cover"
                // Dim image slightly if there's an error overlay
                style={{ opacity: bgRemovalError ? 0.5 : 1 }}
              />
            )}
            {processedImageUrl && (
              <img
                src={processedImageUrl}
                alt="Image with background removed"
                className="h-full w-full object-cover" // Show processed image even if error occurred previously
              />
            )}
          </div>

          <DialogFooter className="mt-auto flex items-center gap-1.5">
            <Button
              variant="outline"
              onClick={() => {
                setIsBgRemovalDialogOpen(false)
                setProcessedImageUrl(null)
                setBgRemovalError(null)
                setIsProcessingBackground(false) // Ensure loading state is reset
              }}
            >
              Cancel
            </Button>

            <Button
              onClick={() => {
                if (selectedImage && processedImageUrl) {
                  setImages(
                    images.map((img, index) =>
                      index === selectedImage - 1
                        ? {
                            ...img,
                            image: processedImageUrl,
                            style: {
                              ...img.style,
                              shadowName: 'None',
                              imageShadow: '0 0 0 0',
                            },
                          }
                        : img
                    )
                  )
                }
                setIsBgRemovalDialogOpen(false)
                setProcessedImageUrl(null)
              }}
              disabled={
                isProcessingBackground || !processedImageUrl || !!bgRemovalError
              } // Disable if processing, no result, or error
              className="flex-center gap-1.5"
            >
              Done
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </Dialog>
  )
}

function ReplaceImage() {
  const { setImages, images } = useImageOptions()
  const { selectedImage, setSelectedImage } = useSelectedLayers()

  const { setImagesCheck, imagesCheck } = useColorExtractor()

  const onDrop = async (file: any) => {
    const analyze = (await import('rgbaster')).default
    if (file) {
      const imageUrl = URL.createObjectURL(file)

      const result = await analyze(imageUrl, {
        scale: 0.3,
      })
      const extractedColors = result.slice(0, 12)

      selectedImage &&
        setImages(
          images.map((image, index) =>
            index === selectedImage - 1
              ? {
                  ...image,
                  image: imageUrl,
                  colors: extractedColors,
                }
              : image
          )
        )

      setImagesCheck([...imagesCheck, imageUrl])
    }
  }
  return (
    <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">
      <label className="ml-6" htmlFor="file-replace">
        Replace image
      </label>
      <input
        id="file-replace"
        name="file-replace"
        type="file"
        onChange={(e: ChangeEvent<HTMLInputElement>) => {
          onDrop(e.target.files?.[0])
        }}
        accept="image/*"
        className="sr-only"
      />
      <span className="ml-auto text-xs tracking-widest text-muted-foreground">
        <ImagePlus size={19} className="opacity-80" />
      </span>
    </div>
  )
}


================================================
FILE: components/editor/image-options/add-image-button.tsx
================================================
import { ChangeEvent, useRef, useState } from 'react'
import { X, Plus, Upload } from 'lucide-react'
import { useImageOptions } from '@/store/use-image-options'
import { useColorExtractor } from '@/store/use-color-extractor'
import { calculateEqualCanvasSize } from '@/utils/helper-fns'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import Dropzone from 'react-dropzone'

type AddImageButtonProps = {}

export default function AddImageButton({}: AddImageButtonProps) {
  const { setImages, images, defaultStyle } = useImageOptions()
  const { imagesCheck, setImagesCheck } = useColorExtractor()
  const uploadRef = useRef<HTMLInputElement>(null)
  const { automaticResolution, setResolution } = useResizeCanvas()
  const [isDragging, setIsDragging] = useState<boolean>(false)

  const handleImageUpload = (file: File) => {
    const imageUrl = URL.createObjectURL(file)
    setImages([
      ...images,
      {
        image: imageUrl,
        id: images.length + 1,
        style:
          images.length < 1
            ? defaultStyle
            : {
                ...defaultStyle,
                imageSize: '0.5',
              },
      },
    ])
    setImagesCheck([...imagesCheck, imageUrl])

    if (images.length > 0) return
    if (automaticResolution) {
      const padding = 200
      const img = new Image()
      img.src = imageUrl

      img.onload = () => {
        const { naturalWidth, naturalHeight } = img
        const newResolution = calculateEqualCanvasSize(
          naturalWidth,
          naturalHeight,
          padding
        )
        setResolution(newResolution.toString())
      }
    }
  }

  const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    if (file) {
      handleImageUpload(file)
    }
  }

  const handleDrop = (acceptedFiles: File[]) => {
    if (acceptedFiles.length > 0) {
      handleImageUpload(acceptedFiles[0])
    }
    setIsDragging(false)
  }

  return (
    <div className="mb-4 mt-2 h-[8rem] w-full px-1 text-sm">
      <Dropzone
        multiple={false}
        onDrop={handleDrop}
        onDragEnter={() => setIsDragging(true)}
        onDragLeave={() => setIsDragging(false)}
        accept={{ 'image/*': [] }}
        noClick
      >
        {({ getRootProps, getInputProps, open }) => (
          <div
            {...getRootProps()}
            className={`relative flex h-full w-full flex-col rounded-xl border-2 p-1 transition-all duration-300 ${
              isDragging
                ? 'scale-[1.02] border-[#898aeb] bg-[#898aeb]/10'
                : 'border-[#898aeb]/20 hover:border-[#898aeb]/60'
            }`}
          >
            <div
              className="group relative flex h-full w-full cursor-pointer items-center justify-center overflow-hidden rounded-lg"
              onClick={open}
              tabIndex={0}
              onKeyDown={(e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                  e.preventDefault()
                  open()
                }
              }}
            >
              {isDragging ? (
                <div className="absolute inset-0 flex items-center justify-center rounded-lg bg-gradient-to-br from-[#898aeb]/20 to-[#d8b9e3]/20">
                  <div className="flex flex-col items-center">
                    <Upload
                      className="mb-2 animate-bounce text-[#898aeb]" 
                      size={24}
                    />
                    <span className="text-sm font-medium text-[#898aeb]">
                      Drop here
                    </span>
                  </div>
                </div>
              ) : (
                <div className="flex flex-col items-center">
                  <Plus
                    className="mb-2 cursor-pointer text-purple/60 transition-transform focus:ring-1 group-hover:scale-110 group-hover:text-purple/80"
                    size={26}
                  />
                  <span className="text-sm font-medium text-purple/60">
                    Click or drag image
                  </span>
                </div>
              )}
            </div>
            <input
              {...getInputProps()}
            />
          </div>
        )}
      </Dropzone>
    </div>
  )
}


================================================
FILE: components/editor/image-options/index.tsx
================================================
'use client'

import { Focus, GalleryVerticalEnd } from 'lucide-react'
import SizeOption from './scale-options'
import RoundnessOption from './roundness-option'
import InsetOption from './inset-option'

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from '@/components/ui/accordion'
import { useImageOptions } from '@/store/use-image-options'
import AddImageButton from './add-image-button'
import ShadowSettings from './shadow-settings'

export default function ImageOptions() {
  const { accordionOpen, setAccordionOpen } = useImageOptions()

  return (
    <>
      <AddImageButton />
      <Accordion
        type="single"
        collapsible
        defaultValue={accordionOpen.appearanceOpen ? 'appearance' : ''}
        className="mt-4 w-full"
      >
        <AccordionItem value="appearance">
          <AccordionTrigger
            onClick={() =>
              setAccordionOpen({
                ...accordionOpen,
                appearanceOpen: !accordionOpen.appearanceOpen,
              })
            }
          >
            <h3 className="flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
              <Focus size={20} />
              <span>Appearance</span>
            </h3>
          </AccordionTrigger>
          <AccordionContent>
            <SizeOption />
            <InsetOption />
            <RoundnessOption />
          </AccordionContent>
        </AccordionItem>
      </Accordion>
      <Accordion
        type="single"
        collapsible
        defaultValue={accordionOpen.shadowOpen ? 'shadow' : ''}
        className="mt-2 w-full"
      >
        <AccordionItem value="shadow">
          <AccordionTrigger
            onClick={() =>
              setAccordionOpen({
                ...accordionOpen,
                shadowOpen: !accordionOpen.shadowOpen,
              })
            }
          >
            <h3 className="flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
              <GalleryVerticalEnd size={20} className="rotate-90" />
              <span>Shadow</span>
            </h3>
          </AccordionTrigger>
          <AccordionContent>
            <ShadowSettings />
          </AccordionContent>
        </AccordionItem>
      </Accordion>
    </>
  )
}


================================================
FILE: components/editor/image-options/inset-option.tsx
================================================
'use client'

import { Slider } from '@/components/ui/slider'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'
import { Button } from '@/components/ui/button'

export default function InsetOption() {
  const { images, updateImageStyle, getImage } = useImageOptions()
  const { setShowControls, showControls } = useMoveable()
  const { selectedImage } = useSelectedLayers()

  return (
    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
      <div className="mb-3 mt-6 flex items-center px-1 md:max-w-full">
        <h1 className="text-[0.85rem]">Inset</h1>
        {images.length !== 0 && (
          <Popover>
            <PopoverTrigger asChild>
              <Button
                className="ml-2 h-5 w-5 rounded-md"
                style={{
                  backgroundColor: selectedImage
                    ? getImage(selectedImage)?.style.insetColor
                    : '#fff',
                }}
                variant="outline"
              />
            </PopoverTrigger>
            <PopoverContent
              align="start"
              className="w-[250px] rounded-lg bg-formDark p-4"
            >
              <div className="grid gap-4">
                <div className="space-y-2">
                  <h4 className="pb-1 text-sm font-medium tracking-tight">
                    Detected colors
                  </h4>
                  <hr className="border-border pt-2" />
                  <div className="flex flex-wrap gap-2">
                    {images[selectedImage! - 1]?.extractedColors?.map(
                      (color) => (
                        <button
                          key={color.color}
                          className={`h-7 w-7 rounded-sm ${
                            images[selectedImage! - 1]?.style.insetColor ===
                            color.color
                              ? 'ring-2 ring-ring ring-offset-2'
                              : ''
                          }`}
                          style={{ backgroundColor: color.color }}
                          onClick={() => {
                            selectedImage &&
                              updateImageStyle(selectedImage, {
                                insetColor: color.color,
                              })
                          }}
                        />
                      )
                    )}
                  </div>
                </div>
              </div>
            </PopoverContent>
          </Popover>
        )}
      </div>

      <div className="flex gap-4 px-1 text-[0.85rem] md:max-w-full">
        <Slider
          defaultValue={[0]}
          max={150}
          min={0}
          step={0.02}
          onValueChange={(value: number[]) => {
            setShowControls(false)
            selectedImage &&
              updateImageStyle(selectedImage, {
                insetSize: value[0].toString(),
              })
          }}
          value={
            images.length !== 0 && selectedImage
              ? [+(getImage(selectedImage)?.style.insetSize ?? 0)]
              : [10]
          }
          onValueCommit={() => setShowControls(true)}
          onIncrement={() => {
            setShowControls(false)
            selectedImage &&
              updateImageStyle(selectedImage, {
                insetSize:
                  +(getImage(selectedImage)?.style.insetSize ?? 0) <= 149
                    ? (
                        +(getImage(selectedImage)?.style.insetSize ?? 0) + 4
                      ).toString()
                    : '150',
              })
          }}
          onDecrement={() => {
            setShowControls(false)
            selectedImage &&
              updateImageStyle(selectedImage, {
                insetSize:
                  +(getImage(selectedImage)?.style.insetSize ?? 0) >= 0
                    ? (
                        +(getImage(selectedImage)?.style.insetSize ?? 0) - 4
                      ).toString()
                    : '0',
              })
          }}
        />
      </div>
    </div>
  )
}


================================================
FILE: components/editor/image-options/roundness-option.tsx
================================================
'use client'

import { Slider } from '@/components/ui/slider'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'

export default function RoundnessOption() {
  const { images, updateImageStyle, getImage } = useImageOptions()
  const { setShowControls } = useMoveable()
  const { selectedImage } = useSelectedLayers()

  const browserFrame = selectedImage ? getImage(selectedImage)?.frame : 'None'

  return (
    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
      <div className="mb-3 mt-6 flex items-center px-1 md:max-w-full">
        <h1 className="text-[0.85rem]">Roundness</h1>
        <p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
          {`${Math.round(
            Number(
              selectedImage
                ? getImage(selectedImage)?.style.imageRoundness
                : 0.2
            ) * 10
          )} `}
        </p>
      </div>

      <div className="flex gap-4 px-1 text-[0.85rem] md:max-w-full">
        <Slider
          defaultValue={[0.7]}
          max={browserFrame !== 'None' && browserFrame !== 'Arc' ? 1.6 : 5}
          min={0}
          step={0.05}
          onValueChange={(value) => {
            setShowControls(false)
            selectedImage &&
              updateImageStyle(selectedImage, { imageRoundness: value[0] })
          }}
          value={
            images.length !== 0 && selectedImage
              ? [+(getImage(selectedImage)?.style.imageRoundness ?? 1)]
              : [1]
          }
          onValueCommit={() => setShowControls(true)}
          onIncrement={() => {
            if (images.length === 0 || !selectedImage) return
            if (Number(getImage(selectedImage)?.style.imageRoundness) >= 5)
              return
            setShowControls(false)
            updateImageStyle(selectedImage, {
              imageRoundness:
                Number(getImage(selectedImage)?.style.imageRoundness ?? 0) +
                0.1,
            })
          }}
          onDecrement={() => {
            if (images.length === 0 || !selectedImage) return
            if (Number(getImage(selectedImage)?.style.imageRoundness) <= 0)
              return
            setShowControls(false)
            updateImageStyle(selectedImage, {
              imageRoundness:
                Number(getImage(selectedImage)?.style.imageRoundness ?? 0) -
                0.1,
            })
          }}
        />
      </div>
    </div>
  )
}


================================================
FILE: components/editor/image-options/scale-options.tsx
================================================
import { Slider } from '@/components/ui/slider'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'

type SizeOptionProps = {
  text?: string
}

export default function SizeOption({ text = 'Scale' }: SizeOptionProps) {
  const { images, setImages, scale, setScale } = useImageOptions()
  const { selectedImage } = useSelectedLayers()
  const { setShowControls } = useMoveable()

  return (
    <>
      <div className="mb-3 mt-2 flex items-center px-1 md:md:max-w-full">
        <h1 className="text-[0.85rem]">{text}</h1>
        <p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
          {Math.round(scale * 100)}%
        </p>
      </div>

      <div className="flex gap-4 px-1 text-[0.85rem] md:md:max-w-full">
        <Slider
          defaultValue={[1]}
          max={3}
          min={0.25}
          step={0.01}
          onValueChange={(value: number[]) => {
            setShowControls(false)
            setScale(value[0])
          }}
          onValueCommit={() => setShowControls(true)}
          value={[scale ?? 1]}
          onIncrement={() => {
            if (scale >= 3) return
            setScale(scale + 0.05)
          }}
          onDecrement={() => {
            if (scale <= 0.25) return
            setScale(scale - 0.05)
          }}
        />
      </div>
    </>
  )
}


================================================
FILE: components/editor/image-options/shadow-settings.tsx
================================================
'use client'

import PopupColorPicker from '@/components/popup-color-picker'
import { Button } from '@/components/ui/button'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { shadows } from '@/utils/presets/shadows'
import { ChevronDown } from 'lucide-react'
import { Slider } from '@/components/ui/slider'
import { useMoveable } from '@/store/use-moveable'

export default function ShadowSettings() {
  const { images, setImages, defaultStyle } = useImageOptions()
  const { showControls } = useMoveable()
  const { selectedImage } = useSelectedLayers()

  const { backgroundType } = useBackgroundOptions()

  const boxShadowStyle = {
    boxShadow: selectedImage
      ? images[selectedImage - 1]?.style.imageShadow
      : '',
  }

  const boxShadowPreview = {
    boxShadow: selectedImage
      ? images[selectedImage - 1]?.style.shadowPreview
      : '',
  }

  const backgroundStyle = {
    backgroundImage: `var(--gradient-bg)`,
    backgroundColor:
      backgroundType === 'mesh' ? `var(--mesh-bg)` : 'var(--solid-bg)',
  }

  const handleShadowButtonClick = (shadow: {
    shadow: string
    fullName: string
    preview: string
  }) => {
    selectedImage &&
      setImages(
        images.map((image, index) =>
          index === selectedImage - 1
            ? {
                ...image,
                style: {
                  ...image.style,
                  imageShadow: shadow.shadow,
                  shadowName: shadow.fullName,
                  shadowPreview: shadow.preview,
                },
              }
            : image
        )
      )
  }

  const handleColorChange = (color: string) => {
    selectedImage &&
      setImages(
        images.map((image, index) =>
          index === selectedImage - 1
            ? {
                ...image,
                style: {
                  ...image.style,
                  shadowColor: color,
                  imageShadow:
                    shadows.find(
                      (shadow) =>
                        shadow.fullName ===
                        (images[selectedImage - 1]?.style.shadowName ?? '')
                    )?.shadow ?? '',
                },
              }
            : image
        )
      )
  }

  return (
    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
      <Popover>
        <PopoverTrigger className="relative mt-2 flex h-14 w-full items-center overflow-hidden rounded-lg border border-border/80 bg-[#898beb05]">
          <div
            style={backgroundStyle}
            className="flex-center h-full basis-[23%]"
          >
            <div
              className="flex-center h-1/2 w-1/2 rounded-md bg-white"
              style={boxShadowPreview}
            ></div>
          </div>
          <div className="flex h-full w-full flex-1 items-center justify-between px-4">
            <div className="flex w-full flex-col items-start">
              <p className="text-[0.85rem] font-medium text-dark/70">
                {selectedImage
                  ? images[selectedImage - 1]?.style.shadowName
                  : 'None'}
              </p>
              <p className="text-[0.7rem] font-bold text-dark/50">
                {selectedImage
                  ? images[selectedImage - 1]?.style.shadowColor.slice(0, 7)
                  : '#000'}
              </p>
            </div>

            <ChevronDown size={18} className="text-dark/80" />
          </div>
        </PopoverTrigger>
        <PopoverContent
          align="start"
          className="grid w-[350px] grid-cols-3 gap-4 rounded-lg bg-formDark p-4"
        >
          {/* Inside popup  */}
          {shadows.map((shadow) => (
            <Button
              variant="secondary"
              key={shadow.name}
              onClick={() => {
                handleShadowButtonClick(shadow)
              }}
              className={`flex-center relative h-20 w-24 cursor-pointer rounded-md ${
                shadow.shadow ===
                  images[selectedImage! - 1]?.style.imageShadow &&
                'outline-none ring-2 ring-ring ring-offset-2'
              }`}
              style={backgroundStyle}
            >
              <div
                className="flex-center h-[75%] w-[95%] rounded-md bg-white text-xs text-[#333]"
                style={{ boxShadow: `${shadow.preview}` }}
              >
                {shadow.name}
              </div>
            </Button>
          ))}
        </PopoverContent>
      </Popover>

      <div className="mb-3 mt-8 flex items-center px-1">
        <h1 className="text-[0.85rem]">Opacity</h1>
        <p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
          {Math.round(
            Number(
              selectedImage
                ? images[selectedImage - 1]?.style.shadowOpacity ?? 0.5
                : 0.5
            ) * 100
          )}
          %
        </p>
      </div>

      <div className="flex gap-4 px-1 text-[0.85rem] md:max-w-full">
        <Slider
          defaultValue={[0.5]}
          min={0}
          max={1}
          step={0.01}
          onValueChange={(value) => {
            selectedImage &&
              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          shadowOpacity: value[0],
                        },
                      }
                    : image
                )
              )
          }}
          value={
            images.length !== 0 && selectedImage
              ? [+images[selectedImage - 1]?.style.shadowOpacity]
              : [1]
          }
          onIncrement={() => {
            if (images.length === 0 || !selectedImage) return
            if (Number(images[selectedImage - 1]?.style.shadowOpacity) >= 1)
              return
            setImages(
              images.map((image, index) =>
                index === selectedImage - 1
                  ? {
                      ...image,
                      style: {
                        ...image.style,
                        shadowOpacity: Number(image.style.shadowOpacity) + 0.01,
                      },
                    }
                  : image
              )
            )
          }}
          onDecrement={() => {
            if (images.length === 0 || !selectedImage) return
            if (Number(images[selectedImage - 1]?.style.shadowOpacity) <= 0)
              return
            setImages(
              images.map((image, index) =>
                index === selectedImage - 1
                  ? {
                      ...image,
                      style: {
                        ...image.style,
                        shadowOpacity: Number(image.style.shadowOpacity) - 0.01,
                      },
                    }
                  : image
              )
            )
          }}
        />
      </div>

      <div className="mb-3 mt-8 flex items-center px-1">
        <h1 className="text-[0.85rem]">Shadow color</h1>
      </div>

      <PopupColorPicker
        shouldShowAlpha={false}
        color={
          selectedImage ? images[selectedImage - 1]?.style.shadowColor : '#000'
        }
        onChange={handleColorChange}
      />
    </div>
  )
}


================================================
FILE: components/editor/main-image-area.tsx
================================================
/* eslint-disable @next/next/no-img-element */
'use client'

import { useOnClickOutside } from '@/hooks/use-on-click-outside'
import demoImage from '@/public/images/demo-tweet.png'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useColorExtractor } from '@/store/use-color-extractor'
import { useFrameOptions } from '@/store/use-frame-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import {
  calculateEqualCanvasSize,
  convertHexToRgba,
  splitWidthHeight,
} from '@/utils/helper-fns'
import { ImageIcon, Upload } from 'lucide-react'
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'
import Dropzone from 'react-dropzone'
import { Button } from '../ui/button'
import BrowserFrame from './browser-frames'
import ContextMenuImage from './image-context-menu'

const ImageUpload = () => {
  const targetRef = useRef<HTMLDivElement>(null)
  const {
    images,
    addImage,
    updateImage,
    updateImageStyle,

    setInitialImageUploaded,
    initialImageUploaded,
  } = useImageOptions()
  const { selectedImage, setSelectedImage, setSelectedText } = useSelectedLayers()
  const { setShowControls, isSelecting, isMultipleTargetSelected } =
    useMoveable()
  const { exactDomResolution } = useResizeCanvas()
  const { width: exactDomWidth, height: exactDomHeight } =
    splitWidthHeight(exactDomResolution)
  const { frameHeight, showStroke, arcDarkMode } = useFrameOptions()
  const { imagesCheck } = useColorExtractor()

  useEffect(() => {
    if (images.length === 0) {
      return
    }
    setInitialImageUploaded(true)

    const extractColors = async () => {
      const analyze = (await import('rgbaster')).default

      const result = await analyze(images[images.length - 1].image, {
        scale: 0.5,
      })

      const extractedColors = result.slice(0, 12)

      updateImage(images.length, { extractedColors })
      updateImageStyle(images.length, {
        insetColor: extractedColors[0].color,
      })
    }
    extractColors()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [imagesCheck])
  console.log(images)

  useOnClickOutside(targetRef, () => {
    if (isMultipleTargetSelected) return
    // setShowControls(false)
  })

  // useOnClickOutside(multipleTargetRef, () => {
  //   setShowControls(false)

  //   setIsSelecting(false)
  // })

  // const {
  //   imageSize,
  //   imageRoundness,
  //   imageShadow,
  //   borderSize,
  //   borderColor,
  //   rotate,
  // } = images[selectedImage - 1]?.style || {}

  return (
    <>
      {!initialImageUploaded && <LoadAImage />}
      {images && (
        <>
          {images.map((image, index) => {
            if (image.image !== '')
              return (
                <ContextMenuImage key={image.id + index}>
                  <div
                    className={`image image-check absolute z-[2] flex-1 overflow-hidden ${
                      isSelecting ? 'selectable' : ''
                    } ${selectedImage ? '' : ''}`}
                    ref={
                      !isMultipleTargetSelected
                        ? image.id === selectedImage
                          ? targetRef
                          : null
                        : targetRef
                    }
                    style={{
                      // transition:
                      //   'box-shadow 0.8s cubic-bezier(0.6, 0.6, 0, 1)',
                      transformStyle: 'preserve-3d',
                      transformOrigin: `50% 50%`,
                      // rotate: `${image.style.rotate}deg`,
                      // transform: `scale(${image.style.imageSize}) rotateX(${image.style.rotateX}deg) rotateY(${image.style.rotateY}deg) rotateZ(${image.style.rotateZ}deg) `,
                      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)`,
                      borderRadius: `${image.style.imageRoundness}rem`,
                      boxShadow:
                        image.style.shadowName !== 'Medium'
                          ? `${image.style.imageShadow} ${convertHexToRgba(
                              image.style.shadowColor,
                              image.style.shadowOpacity
                            )}${
                              image.frame === 'Shadow'
                                ? ',11px 11px rgba(0,0,0,0.8)'
                                : ''
                            }`
                          : `0px 18px 88px -4px ${convertHexToRgba(
                              image.style.shadowColor,
                              image.style.shadowOpacity
                            )}, 0px 8px 28px -6px ${convertHexToRgba(
                              image.style.shadowColor,
                              image.style.shadowOpacity
                            )}${
                              image.frame === 'Shadow'
                                ? ',11px 11px rgba(0,0,0,0.8)'
                                : ''
                            }`,

                      padding:
                        image.frame !== 'None'
                          ? image.frame === 'Arc'
                            ? frameHeight === 'small'
                              ? '10px'
                              : frameHeight === 'medium'
                              ? '13px'
                              : '15px'
                            : ''
                          : `${image.style.insetSize}px`,

                      backgroundColor:
                        image.style.insetSize !== '0' && image.frame === 'None'
                          ? `${image?.style.insetColor}`
                          : image.frame === 'Arc'
                          ? arcDarkMode
                            ? '#00000050'
                            : '#ffffff50'
                          : image.frame === 'Shadow'
                          ? 'rgba(0,0,0,0.8)'
                          : 'transparent',

                      border:
                        image.frame === 'Arc'
                          ? arcDarkMode
                            ? '1px solid #00000020'
                            : '1px solid #ffffff60'
                          : image.frame === 'Shadow'
                          ? showStroke
                            ? '3px solid rgba(0,0,0,0.8)'
                            : ''
                          : '',

                      zIndex: `${image.style.zIndex}`,
                    }}
                    id={`${image.id}`}
                    onClick={() => {
                      setShowControls(true)
                      setSelectedImage(image.id)
                    }}
                    // on right click too do the same
                    onContextMenu={(e) => {
                      setShowControls(true)
                      setSelectedImage(image.id)
                    }}
                  >
                    <BrowserFrame frame={image.frame || 'None'} />

                    <img
                      draggable={false}
                      className={`pointer-events-none h-full w-full shrink-0 ${
                        image.frame === 'Arc' ? 'shadow-md' : ''
                      }`}
                      id={`img-${image.id}`}
                      src={image.image}
                      alt="Uploaded image"
                      style={{
                        borderRadius:
                          image.frame !== 'None'
                            ? image.frame === 'Arc'
                              ? `calc(${image.style.imageRoundness}rem - 9px)`
                              : ''
                            : `calc(${image.style.imageRoundness}rem - ${image.style.insetSize}px)`,

                        padding:
                          image.frame === 'None'
                            ? ''
                            : `${image.style.insetSize}px`,

                        backgroundColor:
                          image.style.insetSize !== '0' &&
                          image.frame !== 'None'
                            ? `${image?.style.insetColor}`
                            : '',
                      }}
                    />
                  </div>

                  {/* Trying layout feature! */}
                  {/* <div
                    className={`flex flex-col image image-check absolute flex-1 z-[2]   ${
                      isSelecting ? 'selectable' : ''
                    } ${selectedImage ? '' : ''}`}
                    style={{
                      // width: '65%',
                      // maxHeight: '35%',
                      width: `${+image.style.imageSize * +exactDomWidth}px`,
                      maxHeight: `${+image.style.imageSize * +exactDomHeight}px`,
                      // transform: `translate(35%,50%) rotateX(40deg) rotate(50deg) scale(1.3) skew(8deg, 0deg)`,
                      borderRadius: `${image.style.imageRoundness}rem`,
                      boxShadow:
                        image.style.shadowName !== 'Medium'
                          ? `${image.style.imageShadow} ${convertHexToRgba(
                              image.style.shadowColor,
                              image.style.shadowOpacity
                            )}${
                              image.frame === 'Shadow'
                                ? ',11px 11px rgba(0,0,0,0.8)'
                                : ''
                            }`
                          : `0px 18px 88px -4px ${convertHexToRgba(
                              image.style.shadowColor,
                              image.style.shadowOpacity
                            )}, 0px 8px 28px -6px ${convertHexToRgba(
                              image.style.shadowColor,
                              image.style.shadowOpacity
                            )}${
                              image.frame === 'Shadow'
                                ? ',11px 11px rgba(0,0,0,0.8)'
                                : ''
                            }`,
                    }}
                    ref={
                      !isMultipleTargetSelected
                        ? image.id === selectedImage
                          ? targetRef
                          : null
                        : targetRef
                    }
                    id={`${image.id}`}
                    onClick={() => {
                      setShowControls(true)
                      setSelectedImage(image.id)
                    }}
                    onContextMenu={(e) => {
                      setShowControls(true)
                      setSelectedImage(image.id)
                    }}
                  >
                    <BrowserFrame frame={image.frame || 'None'} />
                    <img
                      draggable={false}
                      className={`pointer-events-none h-full w-full shrink-0 object-cover object-center ${
                        image.frame === 'Arc' ? 'shadow-md' : ''
                      }`}
                      id={`img-${image.id}`}
                      src={image.image}
                      alt="Uploaded image"
                      style={{
                        borderRadius:
                          image.frame !== 'None'
                            ? image.frame === 'Arc'
                              ? `calc(${image.style.imageRoundness}rem - 9px)`
                              : ''
                            : `calc(${image.style.imageRoundness}rem - ${image.style.insetSize}px)`,

                        padding:
                          image.frame === 'None'
                            ? ''
                            : `${image.style.insetSize}px`,

                        backgroundColor:
                          image.style.insetSize !== '0' &&
                          image.frame !== 'None'
                            ? `${image?.style.insetColor}`
                            : '',
                      }}
                    />
                  </div> */}
                </ContextMenuImage>
              )
          })}
        </>
      )}
    </>
  )
}

export default ImageUpload

function LoadAImage() {
  const {
    images,
    addImage,
    updateImage,
    defaultStyle,
    setInitialImageUploaded,
  } = useImageOptions()
  const { setSelectedImage } = useSelectedLayers()
  const { imagesCheck, setImagesCheck } = useColorExtractor()
  const { setResolution, automaticResolution } = useResizeCanvas()
  const { setBackground } = useBackgroundOptions()
  const [isDragging, setIsDragging] = useState<boolean>(false)

  useEffect(() => {
    const handlePaste = async (event: ClipboardEvent) => {
      const items = event.clipboardData?.items
      if (!items) return

      const itemsArray = Array.from(items)
      for (const item of itemsArray) {
        if (item.type.indexOf('image') === 0) {
          const file = item.getAsFile()
          if (file) {
            const imageUrl = URL.createObjectURL(file)
            setInitialImageUploaded(true)
            setImagesCheck([...imagesCheck, imageUrl])
            addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })
            setSelectedImage(images.length + 1)

            if (images.length === 0 && automaticResolution) {
              const padding = 250
              const img = new Image()
              img.src = imageUrl

              img.onload = () => {
                const { naturalWidth, naturalHeight } = img
                const newResolution = calculateEqualCanvasSize(
                  naturalWidth,
                  naturalHeight,
                  padding
                )
                setResolution(newResolution.toString())
              }
            }
          }
        }
      }
    }

    document.addEventListener('paste', handlePaste)
    return () => document.removeEventListener('paste', handlePaste)
  }, [images, imagesCheck, addImage, setImagesCheck, setInitialImageUploaded, setSelectedImage, defaultStyle, automaticResolution, setResolution])

  const handleImageLoad = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      const file = event.target.files?.[0]

      if (file) {
        const imageUrl = URL.createObjectURL(file)
        setInitialImageUploaded(true)

        setImagesCheck([...imagesCheck, imageUrl])
        addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })
        setSelectedImage(images.length + 1)

        if (images.length > 0) return
        if (automaticResolution) {
          const padding = 200
          const img = new Image()
          img.src = imageUrl

          img.onload = () => {
            const { naturalWidth, naturalHeight } = img
            const newResolution = calculateEqualCanvasSize(
              naturalWidth,
              naturalHeight,
              padding
            )
            setResolution(newResolution.toString())
          }
        }
      }
    },
    [
      setInitialImageUploaded,
      setImagesCheck,
      imagesCheck,
      images,
      defaultStyle,
      setSelectedImage,
      automaticResolution,
      setResolution,
    ]
  )

  const handleImageChange = useCallback(
    (file: any) => {
      // const file = event.target.files?.[0]

      if (file) {
        const imageUrl = URL.createObjectURL(file)
        setInitialImageUploaded(true)

        setImagesCheck([...imagesCheck, imageUrl])
        addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })
        setSelectedImage(images.length + 1)

        if (images.length > 0) return
        if (automaticResolution) {
          const padding = 250
          const img = new Image()
          img.src = imageUrl

          img.onload = () => {
            const { naturalWidth, naturalHeight } = img
            const newResolution = calculateEqualCanvasSize(
              naturalWidth,
              naturalHeight,
              padding
            )
            setResolution(newResolution.toString())
          }
        }
      }
    },
    [
      setInitialImageUploaded,
      setImagesCheck,
      imagesCheck,
      images,
      defaultStyle,
      setSelectedImage,
      automaticResolution,
      setResolution,
    ]
  )

  const loadDemoImage = () => {
    if (typeof window === 'undefined') return
    setBackground('linear-gradient(var(--gradient-angle), #898aeb, #d8b9e3)')
    document?.documentElement.style.setProperty(
      '--gradient-bg',
      ' linear-gradient(var(--gradient-angle), #898aeb, #d8b9e3)'
    )
    addImage({
      image: demoImage.src,
      id: 1,
      style: {
        ...defaultStyle,
        borderSize: '15',
        imageRoundness: 0.7,
        imageSize: '0.78',
        insetSize: '10',
      },
    })
    setImagesCheck([...imagesCheck, demoImage.src])
    setResolution('1920x1080')
  }

  return (
    <Dropzone
      multiple={false}
      onDrop={(acceptedFiles) => {
        handleImageChange(acceptedFiles[0])
      }}
      onDragEnter={() => setIsDragging(true)}
      onDragLeave={() => setIsDragging(false)}
      noClick
    >
      {({ getRootProps, getInputProps }) => (
        <div
          {...getRootProps()}
          className="h-25 absolute-center w-4/5 xl:w-2/5"
        >
          <div className="flex flex-col gap-4 rounded-xl  text-center  md:shadow-2xl">
            <div className="flex-center flex-col rounded-xl px-4 py-10 md:bg-[#f1f1f1]">
              <Upload
                style={{
                  transition: 'all 0.8s cubic-bezier(0.6, 0.6, 0, 1)',
                }}
                className={`mx-auto mb-2 hidden h-10 w-10 text-[#332]/80 sm:block ${
                  isDragging ? 'rotate-180' : 'rotate-0'
                }`}
                aria-hidden="true"
              />
              <div className="flex-center mt-4 text-base leading-6 text-gray-400">
                <label
                  htmlFor="file-upload"
                  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"
                >
                  <span>Load a image</span>
                </label>
                <input {...getInputProps()} />
                <input
                  id="file-upload"
                  name="file-upload"
                  type="file"
                  onChange={handleImageLoad}
                  accept="image/*"
                  className="sr-only"
                />
                <p className="hidden pl-1 font-medium text-[#333]/80 lg:block">
                  or drag and drop
                </p>
              </div>

              <p className="mt-4 hidden text-sm font-extrabold leading-5 text-[#555]/80 sm:block">
                OR
              </p>

              <Button
                onClick={loadDemoImage}
                className="z-[120] mt-4 hidden rounded-md border-[#898aeb]/40 bg-[#898aeb]/30 font-semibold text-[#6264aa] shadow-sm sm:inline-flex"
                variant="stylish"
              >
                Try with a demo image
                <ImageIcon className="ml-2" size={19} />
              </Button>
            </div>
          </div>
        </div>
      )}
    </Dropzone>
  )
}


================================================
FILE: components/editor/mobile-view-image-options.tsx
================================================
import { ScrollArea } from '@/components/ui/scroll-area'
import BackgroundOptions from '@/components/editor/background-options'
import CanvasOptions from '@/components/editor/canvas-options'
import FrameOptions from '@/components/editor/frame-options'
import ImageOptions from '@/components/editor/image-options'
import PerspectiveOptions from '@/components/editor/perspective-options'
import PositionOptions from '@/components/editor/position-options'
import TextOptions from '@/components/editor/text-options'
import { useActiveIndexStore } from '@/store/use-active-index'

export default function MobileViewImageOptions() {
  const { activeIndex } = useActiveIndexStore()

  return (
    <ScrollArea className="mt-6 w-full md:hidden" type="auto">
      <div className="w-full max-w-[90%] md:hidden">
        {activeIndex === 0 && <CanvasOptions />}
        {activeIndex === 1 && <ImageOptions />}
        {activeIndex === 2 && <BackgroundOptions />}
        {activeIndex === 3 && <FrameOptions />}
        {activeIndex === 4 && <TextOptions />}
        {activeIndex === 5 && <PerspectiveOptions />}
        {activeIndex === 6 && <PositionOptions />}
      </div>
    </ScrollArea>
  )
}


================================================
FILE: components/editor/moveable-component.tsx
================================================
'use client'

import React from 'react'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useResizeCanvas } from '@/store/use-resize-canvas'

import { useImageQualityStore } from '@/store/use-image-quality'
import { useMoveable } from '@/store/use-moveable'
import { splitWidthHeight } from '@/utils/helper-fns'
import {
  Draggable,
  DraggableProps,
  GroupableProps,
  Rotatable,
  RotatableProps,
  Scalable,
  ScalableProps,
  Snappable,
  SnappableProps,
  makeMoveable,
} from 'react-moveable'

const Moveable = makeMoveable<
  DraggableProps &
    ScalableProps &
    RotatableProps &
    SnappableProps &
    GroupableProps
  // @ts-ignore
>([Draggable, Scalable, Rotatable, Snappable])

export default function MoveableComponent({ id }: { id: string }) {
  const { quality } = useImageQualityStore()
  const { domResolution, scaleFactor, exactDomResolution } = useResizeCanvas()
  const { setImages, images } = useImageOptions()
  const { selectedImage } = useSelectedLayers()
  const moveableRef = React.useRef<any>(null)
  const { width, height } = splitWidthHeight(exactDomResolution)
  const { isMultipleTargetSelected } = useMoveable()

  const otherImages = images.filter((image) => image.id !== selectedImage)
  const elementGuidelines = otherImages.map((image) => ({
    element:
      typeof document !== 'undefined'
        ? document?.getElementById(`${image.id}`)
        : '',
  }))

  const [domWidth, domHeight]: number[] = domResolution.split('x').map(Number)
  return (
    <Moveable
      ref={moveableRef}
      target={
        isMultipleTargetSelected
          ? '.selected'
          : typeof document !== 'undefined'
          ? document?.getElementById(id)
          : ''
      }
      hideChildMoveableDefaultLines={true}
      draggable={true}
      onDrag={({ target, beforeTranslate }) => {
        const perspective = target.style.transform.match(/perspective\((.*?)\)/)
        // @ts-expect-error
        const xPerc = (beforeTranslate[0] / target?.offsetWidth) * 100
        // @ts-expect-error
        const yPerc = (beforeTranslate[1] / target?.offsetHeight) * 100

        const scale = target.style.transform.match(/scale\((.*?)\)/)

        const rotate = target.style.transform.match(/rotate\((.*?)\)/)

        const rotateX = target.style.transform.match(/rotateX\((.*?)\)/)
        const rotateY = target.style.transform.match(/rotateY\((.*?)\)/)
        const rotateZ = target.style.transform.match(/rotateZ\((.*?)\)/)

        target.style.transform = `${
          perspective ? perspective[0] : ''
        } translate(${xPerc}%, ${yPerc}%) ${scale ? scale[0] : ''} ${
          rotate ? rotate[0] : ''
        } ${rotateX ? rotateX[0] : ''} ${rotateY ? rotateY[0] : ''} ${
          rotateZ ? rotateZ[0] : ''
        } `
      }}
      onDragEnd={({ target, lastEvent }) => {
        // @ts-expect-error
        const xPerc = (lastEvent?.translate[0] / target?.offsetWidth) * 100
        // @ts-expect-error
        const yPerc = (lastEvent?.translate[1] / target?.offsetHeight) * 100

        selectedImage &&
          setImages(
            images.map((image, index) =>
              index === selectedImage - 1
                ? {
                    ...image,
                    style: {
                      ...image.style,
                      translateX: xPerc,
                      translateY: yPerc,
                    },
                  }
                : image
            )
          )
      }}
      scalable={true}
      keepRatio={true}
      onScale={({ scale, target, drag }) => {
        const perspective = target.style.transform.match(/perspective\((.*?)\)/)
        const rotateX = target.style.transform.match(/rotateX\((.*?)\)/)

        const scaleX = scale[0]
        const scaleY = scale[1]

        const rotate = target.style.transform.match(/rotate\((.*?)\)/)

        const rotateY = target.style.transform.match(/rotateY\((.*?)\)/)
        const rotateZ = target.style.transform.match(/rotateZ\((.*?)\)/)

        // @ts-expect-error
        const xPerc = (drag.beforeTranslate[0] / target.offsetWidth) * 100
        // @ts-expect-error
        const yPerc = (drag.beforeTranslate[1] / target.offsetHeight) * 100

        target.style.transform = `${
          perspective ? perspective[0] : ''
        } translate(${xPerc}%, ${yPerc}%) scale(${scaleX}, ${scaleY}) ${
          rotate ? rotate[0] : ''
        } ${rotateX ? rotateX[0] : ''} ${rotateY ? rotateY[0] : ''} ${
          rotateZ ? rotateZ[0] : ''
        }`
      }}
      onScaleEnd={({ target, lastEvent }) => {
        const scaleX = lastEvent.scale[0]
        // @ts-expect-error
        const xPerc = (lastEvent?.drag?.translate[0] / target.offsetWidth) * 100
        const yPerc =
          // @ts-expect-error
          (lastEvent?.drag?.translate[1] / target.offsetHeight) * 100

        selectedImage &&
          setImages(
            images.map((image, index) =>
              index === selectedImage - 1
                ? {
                    ...image,
                    style: {
                      ...image.style,
                      translateX: xPerc,
                      translateY: yPerc,
                      imageSize: `${scaleX}`,
                    },
                  }
                : image
            )
          )
      }}
      rotatable={!isMultipleTargetSelected}
      rotationPosition={'top'}
      onRotate={({ target, beforeRotate }) => {
        const scale = target.style.transform.match(/scale\((.*?)\)/)
        const translate = target.style.transform.match(/translate\((.*?)\)/)

        const perspective = target.style.transform.match(/perspective\((.*?)\)/)
        const rotateX = target.style.transform.match(/rotateX\((.*?)\)/)
        const rotateY = target.style.transform.match(/rotateY\((.*?)\)/)
        const rotateZ = target.style.transform.match(/rotateZ\((.*?)\)/)

        const rotate = beforeRotate || ''

        target.style.transform = `${perspective ? perspective[0] : ''} ${
          translate ? translate[0] : ''
        } ${scale ? scale[0] : ''} ${rotate ? `rotate(${rotate}deg)` : ''} ${
          rotateX ? rotateX[0] : ''
        } ${rotateY ? rotateY[0] : ''} ${rotateZ ? rotateZ[0] : ''}`
      }}
      onRotateEnd={({ target, lastEvent }) => {
        const rotate = lastEvent.rotate

        selectedImage &&
          setImages(
            images.map((image, index) =>
              index === selectedImage - 1
                ? {
                    ...image,
                    style: {
                      ...image.style,
                      rotate: rotate,
                    },
                  }
                : image
            )
          )
      }}
      snapRotationThreshold={2}
      snapRotationDegrees={[0, 90, 180, 270]}
      snappable={true}
      snapDirections={{
        center: true,
        middle: true,
        left: true,
        top: true,
        right: true,
        bottom: true,
      }}
      snapThreshold={7}
      horizontalGuidelines={[
        domHeight / 2 / scaleFactor / quality,
        domHeight / 1 / scaleFactor / quality,
        0,
      ]}
      verticalGuidelines={[
        domWidth / 2 / scaleFactor / quality,
        domWidth / 1 / scaleFactor / quality,
        0,
      ]}
      elementSnapDirections={{
        top: true,
        left: true,
        bottom: true,
        right: true,
        center: true,
        middle: true,
      }}
      elementGuidelines={!isMultipleTargetSelected ? elementGuidelines : []}
      onRenderGroup={({ targets, events }) => {
        if (!isMultipleTargetSelected) return

        events.forEach((ev) => {
          ev.target.style.transform = ev.transform
        })
      }}
      onRenderGroupEnd={({ targets, events }) => {
        if (isMultipleTargetSelected) {
          const updatedImages = images.map((image, index) => {
            const targetIndex = events.findIndex(
              (ev: any) => ev.target.id === `${image.id}`
            )
            const updatedEvent = events[targetIndex]

            if (targetIndex !== -1) {
              const updatedEvent = events[targetIndex]

              const xPerc =
                (updatedEvent?.transformObject?.translate[0] /
                  // @ts-expect-error
                  targets[targetIndex]?.offsetWidth) *
                100
              const yPerc =
                (updatedEvent?.transformObject?.translate[1] /
                  // @ts-expect-error
                  targets[targetIndex]?.offsetHeight) *
                100

              return {
                ...image,
                style: {
                  ...image.style,
                  translateX: xPerc,
                  translateY: yPerc,
                  imageSize: `${updatedEvent.transformObject.scale[0]}`,
                },
              }
            }

            // Return the original image if the target is not found
            return image
          })

          setImages(updatedImages)
        }
      }}
    />
  )
}


================================================
FILE: components/editor/noise.tsx
================================================
import { useBackgroundOptions } from '@/store/use-background-options'

export default function Noise() {
  const { noise, isBackgroundClicked } = useBackgroundOptions()
  return (
    <>
      {/* eslint-disable-next-line @next/next/no-img-element */}
     {isBackgroundClicked && <img
        draggable={false}
        className={`pointer-events-none absolute z-[0] h-full w-full object-cover`}
        style={{
          opacity: noise,
        }}
        src={'/images/Noise.svg'}
        alt="noise"
        loading="lazy"
      />}
    </>
  )
}


================================================
FILE: components/editor/perspective-options/index.tsx
================================================
import { JoystickIcon, Rotate3d } from 'lucide-react'
import RotateOptions from './rotate-options'
import { Joystick } from 'react-joystick-component'
import { IJoystickUpdateEvent } from 'react-joystick-component/build/lib/Joystick'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'

export default function PerspectiveOptions() {
  const { images, setImages } = useImageOptions()
   const { selectedImage } = useSelectedLayers()
  const { setShowControls } = useMoveable()

  return (
    <div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
      <h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
        <Rotate3d size={20} />
        <span>Custom options</span>
      </h3>
      <RotateOptions />
      <hr className="my-8" />
      <h3 className="mb-6 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
        <JoystickIcon size={20} />
        <span>Or with a controller</span>
      </h3>
      <Joystick
        size={40}
        stickColor="#898aeb"
        baseColor="#898aeb40"
        move={(event: IJoystickUpdateEvent) => {
          const { type, x, y } = event
          if (type === 'move') {
            setShowControls(false)
            selectedImage &&
              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          rotateX: y! * 20,
                          rotateY: x! * 20,
                        },
                      }
                    : image
                )
              )
          } else if (type === 'stop') {
            setShowControls(true)
          }
        }}
      ></Joystick>
    </div>
  )
}


================================================
FILE: components/editor/perspective-options/rotate-options.tsx
================================================
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { useMoveable } from '@/store/use-moveable'
import { RotateCcw } from 'lucide-react'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'

export default function RotateOptions() {
  const { images, setImages } = useImageOptions()
  const { selectedImage } = useSelectedLayers()
  const { setShowControls } = useMoveable()

  return (
    <>
      {/* Perspective */}
      <div
        className={`mb-3 mt-8 flex items-center px-1 md:max-w-full ${
          selectedImage ? '' : 'pointer-events-none opacity-40'
        }`}
      >
        <h1 className="text-[0.85rem]">3D Depth</h1>
        <p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
          {`${Math.round(
            selectedImage ? images[selectedImage - 1]?.style.perspective : 0
          )}px`}
        </p>
        <Button
          aria-label="reset rotate x"
          variant="secondary"
          size="sm"
          className="ml-auto translate-x-2"
          onClick={() =>
            selectedImage &&
            setImages(
              images.map((image, index) =>
                index === selectedImage - 1
                  ? {
                      ...image,
                      style: {
                        ...image.style,
                        perspective: 2000,
                      },
                    }
                  : image
              )
            )
          }
        >
          <RotateCcw size={15} className="text-dark/80" />
        </Button>
      </div>

      <div className="mb-3 flex gap-4 text-[0.85rem] md:max-w-full">
        <Slider
          defaultValue={[0]}
          max={6500}
          min={0}
          step={0.0001}
          value={
            selectedImage
              ? [images[selectedImage - 1]?.style.perspective]
              : [2000]
          }
          onValueChange={(value: number[]) => {
            selectedImage &&
              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          perspective: value[0],
                        },
                      }
                    : image
                )
              )
            setShowControls(false)
          }}
          onValueCommit={() => setShowControls(true)}
          onIncrement={() => {
            if (selectedImage) {
              if (images[selectedImage - 1]?.style.perspective >= 6500) return

              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          perspective: Number(image.style.perspective) + 500,
                        },
                      }
                    : image
                )
              )
            }
          }}
          onDecrement={() => {
            if (selectedImage) {
              if (images[selectedImage - 1]?.style.perspective <= 0) return

              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          perspective: Number(image.style.perspective) - 500,
                        },
                      }
                    : image
                )
              )
            }
          }}
        />
      </div>

      <hr className="my-6" />

      {/* RotateX */}
      <div className="mb-3 flex items-center px-1 md:max-w-full">
        <h1 className="text-[0.85rem]">Rotate X</h1>
        <p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
          {`${Math.round(
            selectedImage ? images[selectedImage - 1]?.style.rotateX : 0
          )}px`}
        </p>
        <Button
          aria-label="reset rotate x"
          variant="secondary"
          size="sm"
          className="ml-auto translate-x-2"
          onClick={() => {
            selectedImage &&
              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          rotateX: 0.0001,
                        },
                      }
                    : image
                )
              )
          }}
        >
          <RotateCcw size={15} className="text-dark/80" />
        </Button>
      </div>

      <div className="mb-3 flex gap-4 text-[0.85rem] md:max-w-full">
        <Slider
          defaultValue={[0]}
          max={180}
          min={-180}
          step={0.0001}
          value={
            selectedImage ? [images[selectedImage - 1]?.style.rotateX] : [0]
          }
          onValueChange={(value: number[]) => {
            selectedImage &&
              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
                          ...image.style,
                          rotateX: value[0],
                        },
                      }
                    : image
                )
              )
            setShowControls(false)
          }}
          onValueCommit={() => setShowControls(true)}
          onIncrement={() => {
            if (selectedImage) {
              if (images[selectedImage - 1]?.style.rotateX >= 180) return

              setImages(
                images.map((image, index) =>
                  index === selectedImage - 1
                    ? {
                        ...image,
                        style: {
               
Download .txt
gitextract_iv9ddlq0/

├── .eslintrc.json
├── .gitignore
├── .vscode/
│   └── settings.json
├── README.md
├── app/
│   ├── (routes)/
│   │   ├── about/
│   │   │   └── page.tsx
│   │   ├── articles/
│   │   │   ├── [slug]/
│   │   │   │   ├── loading.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── page.tsx
│   │   │   └── sitemap.ts
│   │   └── layout.tsx
│   ├── admin/
│   │   ├── layout.tsx
│   │   └── write-article/
│   │       ├── blog-form.tsx
│   │       └── page.tsx
│   ├── api/
│   │   ├── article/
│   │   │   └── post/
│   │   │       └── route.ts
│   │   ├── auth/
│   │   │   └── [...nextauth]/
│   │   │       └── route.ts
│   │   └── user/
│   │       └── settings/
│   │           └── route.ts
│   ├── error.tsx
│   ├── layout.tsx
│   ├── loading.tsx
│   ├── manifest.json
│   ├── not-found.tsx
│   ├── page.tsx
│   ├── robots.ts
│   └── sitemap.ts
├── components/
│   ├── articles/
│   │   └── article-card.tsx
│   ├── auth-modal.tsx
│   ├── clarity-script.tsx
│   ├── color-picker.tsx
│   ├── editor/
│   │   ├── background-image-canvas.tsx
│   │   ├── background-options/
│   │   │   ├── custom-gradient-picker.tsx
│   │   │   ├── image-gradient-picker.tsx
│   │   │   ├── index.tsx
│   │   │   ├── noise-slider.tsx
│   │   │   ├── normal-gradient-picker.tsx
│   │   │   └── pattern-picker.tsx
│   │   ├── browser-frames.tsx
│   │   ├── canvas-area.tsx
│   │   ├── canvas-options/
│   │   │   ├── canvas-roundness-slider.tsx
│   │   │   ├── index.tsx
│   │   │   └── resolution-button.tsx
│   │   ├── frame-options/
│   │   │   ├── additional-frame-options.tsx
│   │   │   ├── frame-picker.tsx
│   │   │   └── index.tsx
│   │   ├── image-context-menu.tsx
│   │   ├── image-options/
│   │   │   ├── add-image-button.tsx
│   │   │   ├── index.tsx
│   │   │   ├── inset-option.tsx
│   │   │   ├── roundness-option.tsx
│   │   │   ├── scale-options.tsx
│   │   │   └── shadow-settings.tsx
│   │   ├── main-image-area.tsx
│   │   ├── mobile-view-image-options.tsx
│   │   ├── moveable-component.tsx
│   │   ├── noise.tsx
│   │   ├── perspective-options/
│   │   │   ├── index.tsx
│   │   │   └── rotate-options.tsx
│   │   ├── position-options/
│   │   │   ├── index.tsx
│   │   │   ├── position-control.tsx
│   │   │   └── translate-control.tsx
│   │   ├── selecto-component.tsx
│   │   ├── sidebar-buttons.tsx
│   │   ├── sidebar.tsx
│   │   ├── text-context-menu.tsx
│   │   ├── text-layers.tsx
│   │   ├── text-options/
│   │   │   ├── add-text-layer.tsx
│   │   │   ├── font-settings.tsx
│   │   │   └── index.tsx
│   │   ├── tiptap-moveable.tsx
│   │   └── undo-redo-buttons.tsx
│   ├── export-options.tsx
│   ├── footer.tsx
│   ├── icons/
│   │   ├── index.tsx
│   │   └── info.icon.tsx
│   ├── loader.tsx
│   ├── navbar.tsx
│   ├── navlinks.tsx
│   ├── popup-color-picker.tsx
│   ├── profile-dialog.tsx
│   ├── settings-dialog.tsx
│   ├── sign-in-form.tsx
│   ├── spinner/
│   │   ├── spinner.module.css
│   │   └── spinner.tsx
│   ├── ui/
│   │   ├── accordion.tsx
│   │   ├── back-button.tsx
│   │   ├── badge.tsx
│   │   ├── button.tsx
│   │   ├── circular-slider.tsx
│   │   ├── context-menu.tsx
│   │   ├── dialog.tsx
│   │   ├── drawer.tsx
│   │   ├── dropdown-menu.tsx
│   │   ├── gradient-text.tsx
│   │   ├── input.tsx
│   │   ├── popover.tsx
│   │   ├── scroll-area.tsx
│   │   ├── select.tsx
│   │   ├── separator.tsx
│   │   ├── skeleton.tsx
│   │   ├── slider.tsx
│   │   ├── spotlight-button.tsx
│   │   ├── style/
│   │   │   └── checkbox.module.css
│   │   ├── switch.tsx
│   │   ├── tabs.tsx
│   │   ├── text-area.tsx
│   │   ├── text.tsx
│   │   ├── theme-button.tsx
│   │   ├── toast.tsx
│   │   ├── toaster.tsx
│   │   └── tooltip.tsx
│   └── user-dropdown.tsx
├── components.json
├── docker-compose.yml
├── hooks/
│   ├── canvas-area-hooks/
│   │   ├── use-automatic-aspect-ratio-switcher.ts
│   │   ├── use-resize-observer.ts
│   │   └── use-screen-size-warning-toast.ts
│   ├── use-editor.ts
│   ├── use-event-listener.ts
│   ├── use-isomorphic-layout-effect.ts
│   ├── use-media-query.ts
│   ├── use-on-click-outside.ts
│   └── use-toast.ts
├── index.d.ts
├── libs/
│   ├── prismadb.ts
│   └── validators/
│       ├── article-post-validator.ts
│       └── user-settings-validator.ts
├── license.md
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── prettier.config.js
├── prisma/
│   └── schema.prisma
├── providers/
│   └── index.tsx
├── store/
│   ├── use-active-index.ts
│   ├── use-background-options.ts
│   ├── use-color-extractor.ts
│   ├── use-frame-options.ts
│   ├── use-image-options.ts
│   ├── use-image-quality.ts
│   ├── use-moveable.ts
│   ├── use-resize-canvas.ts
│   └── use-tiptap.ts
├── styles/
│   └── globals.css
├── tailwind.config.js
├── tsconfig.json
├── utils/
│   ├── auth-options.ts
│   ├── button-utils.ts
│   ├── helper-fns.ts
│   ├── presets/
│   │   ├── gradients.ts
│   │   ├── qualities.ts
│   │   ├── resolutions.ts
│   │   ├── shadows.ts
│   │   └── solid-colors.ts
│   └── tiptap-extensions.ts
└── workers/
    └── background-removal.worker.ts
Download .txt
SYMBOL INDEX (167 symbols across 113 files)

FILE: app/(routes)/about/page.tsx
  function page (line 21) | function page() {

FILE: app/(routes)/articles/[slug]/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: app/(routes)/articles/[slug]/page.tsx
  type ArticleProps (line 16) | type ArticleProps = {
  function generateStaticParams (line 24) | async function generateStaticParams() {
  function generateMetadata (line 46) | async function generateMetadata(props: ArticleProps) {
  function ArticlePage (line 97) | async function ArticlePage(props: ArticleProps) {

FILE: app/(routes)/articles/page.tsx
  function Article (line 37) | async function Article() {

FILE: app/(routes)/articles/sitemap.ts
  function sitemap (line 4) | async function sitemap(): Promise<MetadataRoute.Sitemap> {

FILE: app/(routes)/layout.tsx
  function AdminLayout (line 4) | function AdminLayout({

FILE: app/admin/layout.tsx
  function AdminLayout (line 3) | function AdminLayout({

FILE: app/admin/write-article/blog-form.tsx
  function BlogForm (line 31) | function BlogForm() {

FILE: app/admin/write-article/page.tsx
  function page (line 15) | function page() {

FILE: app/api/article/post/route.ts
  function POST (line 8) | async function POST(request: Request) {

FILE: app/api/user/settings/route.ts
  function GET (line 7) | async function GET() {
  function PATCH (line 41) | async function PATCH(request: Request) {

FILE: app/error.tsx
  function Error (line 10) | function Error({

FILE: app/layout.tsx
  function RootLayout (line 93) | function RootLayout({

FILE: app/loading.tsx
  function Loading (line 3) | function Loading() {

FILE: app/not-found.tsx
  function NotFound (line 9) | function NotFound() {

FILE: app/page.tsx
  function Home (line 4) | function Home() {

FILE: app/robots.ts
  function robots (line 3) | function robots(): MetadataRoute.Robots {

FILE: app/sitemap.ts
  function sitemap (line 3) | function sitemap(): MetadataRoute.Sitemap {

FILE: components/articles/article-card.tsx
  type ArticleCardProps (line 4) | type ArticleCardProps = {
  function ArticleCard (line 12) | function ArticleCard({

FILE: components/auth-modal.tsx
  function AuthModal (line 10) | function AuthModal() {

FILE: components/color-picker.tsx
  function ColorPicker (line 10) | function ColorPicker({

FILE: components/editor/background-image-canvas.tsx
  function BackgroundImageCanvas (line 6) | function BackgroundImageCanvas() {

FILE: components/editor/background-options/custom-gradient-picker.tsx
  function CustomGradientPicker (line 9) | function CustomGradientPicker() {

FILE: components/editor/background-options/image-gradient-picker.tsx
  function ImageGradientPicker (line 17) | function ImageGradientPicker() {

FILE: components/editor/background-options/index.tsx
  function BackgroundOptions (line 9) | function BackgroundOptions() {

FILE: components/editor/background-options/noise-slider.tsx
  function NoiseSlider (line 5) | function NoiseSlider() {

FILE: components/editor/background-options/normal-gradient-picker.tsx
  type Color (line 29) | type Color = string
  function NormalGradientPicker (line 31) | function NormalGradientPicker() {

FILE: components/editor/background-options/pattern-picker.tsx
  function PatternPicker (line 17) | function PatternPicker() {

FILE: components/editor/browser-frames.tsx
  function BrowserFrame (line 91) | function BrowserFrame({ frame }: { frame: FrameTypes }) {

FILE: components/editor/canvas-area.tsx
  function Canvas (line 26) | function Canvas() {

FILE: components/editor/canvas-options/canvas-roundness-slider.tsx
  function CanvasRoundnessSlider (line 6) | function CanvasRoundnessSlider() {

FILE: components/editor/canvas-options/index.tsx
  function CanvasOptions (line 45) | function CanvasOptions() {

FILE: components/editor/canvas-options/resolution-button.tsx
  function ResolutionButton (line 15) | function ResolutionButton({

FILE: components/editor/frame-options/additional-frame-options.tsx
  function AdditionalFrameOptions (line 7) | function AdditionalFrameOptions() {

FILE: components/editor/frame-options/frame-picker.tsx
  function FramePicker (line 13) | function FramePicker() {
  function FrameContainer (line 149) | function FrameContainer({

FILE: components/editor/frame-options/index.tsx
  function FrameOptions (line 6) | function FrameOptions() {

FILE: components/editor/image-context-menu.tsx
  function ContextMenuImage (line 40) | function ContextMenuImage({
  function ReplaceImage (line 465) | function ReplaceImage() {

FILE: components/editor/image-options/add-image-button.tsx
  type AddImageButtonProps (line 9) | type AddImageButtonProps = {}
  function AddImageButton (line 11) | function AddImageButton({}: AddImageButtonProps) {

FILE: components/editor/image-options/index.tsx
  function ImageOptions (line 18) | function ImageOptions() {

FILE: components/editor/image-options/inset-option.tsx
  function InsetOption (line 13) | function InsetOption() {

FILE: components/editor/image-options/roundness-option.tsx
  function RoundnessOption (line 7) | function RoundnessOption() {

FILE: components/editor/image-options/scale-options.tsx
  type SizeOptionProps (line 5) | type SizeOptionProps = {
  function SizeOption (line 9) | function SizeOption({ text = 'Scale' }: SizeOptionProps) {

FILE: components/editor/image-options/shadow-settings.tsx
  function ShadowSettings (line 17) | function ShadowSettings() {

FILE: components/editor/main-image-area.tsx
  function LoadAImage (line 310) | function LoadAImage() {

FILE: components/editor/mobile-view-image-options.tsx
  function MobileViewImageOptions (line 11) | function MobileViewImageOptions() {

FILE: components/editor/moveable-component.tsx
  function MoveableComponent (line 32) | function MoveableComponent({ id }: { id: string }) {

FILE: components/editor/noise.tsx
  function Noise (line 3) | function Noise() {

FILE: components/editor/perspective-options/index.tsx
  function PerspectiveOptions (line 8) | function PerspectiveOptions() {

FILE: components/editor/perspective-options/rotate-options.tsx
  function RotateOptions (line 7) | function RotateOptions() {

FILE: components/editor/position-options/index.tsx
  function PositionOptions (line 7) | function PositionOptions() {

FILE: components/editor/position-options/position-control.tsx
  function PositionControl (line 17) | function PositionControl() {

FILE: components/editor/position-options/translate-control.tsx
  function TranslateOption (line 7) | function TranslateOption() {

FILE: components/editor/selecto-component.tsx
  function SelectoComponent (line 13) | function SelectoComponent() {

FILE: components/editor/sidebar-buttons.tsx
  function SidebarButton (line 6) | function SidebarButton({

FILE: components/editor/sidebar.tsx
  type SidebarSection (line 25) | type SidebarSection =
  type SidebarButton (line 34) | interface SidebarButton {
  function useSidebarButtons (line 41) | function useSidebarButtons() {
  function Sidebar (line 71) | function Sidebar() {
  type SidebarImageSettingsProps (line 101) | interface SidebarImageSettingsProps {
  function SidebarImageSettings (line 105) | function SidebarImageSettings({

FILE: components/editor/text-context-menu.tsx
  function ContextMenuText (line 15) | function ContextMenuText({

FILE: components/editor/text-layers.tsx
  type MenuBarProps (line 11) | type MenuBarProps = {
  function TipTapEditor (line 77) | function TipTapEditor() {
  function TextLayers (line 103) | function TextLayers() {

FILE: components/editor/text-options/add-text-layer.tsx
  function AddTextLayer (line 7) | function AddTextLayer() {

FILE: components/editor/text-options/font-settings.tsx
  function FontSettings (line 31) | function FontSettings() {

FILE: components/editor/text-options/index.tsx
  function TextOptions (line 6) | function TextOptions() {

FILE: components/editor/tiptap-moveable.tsx
  function TiptapMoveable (line 25) | function TiptapMoveable({ id }: { id: string }) {

FILE: components/editor/undo-redo-buttons.tsx
  function useUndoRedoHotkeys (line 9) | function useUndoRedoHotkeys() {
  function UndoRedoButtons (line 34) | function UndoRedoButtons() {

FILE: components/export-options.tsx
  type ExportOptionsProps (line 18) | interface ExportOptionsProps {
  function ExportOptions (line 22) | function ExportOptions({ isLoggedIn }: ExportOptionsProps) {

FILE: components/footer.tsx
  function Footer (line 6) | function Footer() {

FILE: components/icons/index.tsx
  type IconType (line 3) | type IconType = {
  function Icon (line 12) | function Icon({

FILE: components/icons/info.icon.tsx
  function InfoIcon (line 3) | function InfoIcon({

FILE: components/loader.tsx
  function Loader (line 3) | function Loader() {

FILE: components/navbar.tsx
  function Navbar (line 10) | function Navbar() {

FILE: components/navlinks.tsx
  function NavLinks (line 32) | function NavLinks() {

FILE: components/popup-color-picker.tsx
  function PopupColorPicker (line 7) | function PopupColorPicker({

FILE: components/profile-dialog.tsx
  type ProfileDialogProps (line 21) | interface ProfileDialogProps {
  function ProfileDialog (line 26) | function ProfileDialog({

FILE: components/settings-dialog.tsx
  type SettingsDialogProps (line 30) | interface SettingsDialogProps {
  function SettingsDialog (line 35) | function SettingsDialog({

FILE: components/sign-in-form.tsx
  type SignInFormProps (line 12) | type SignInFormProps = {
  function SignInForm (line 16) | function SignInForm({ authenticated }: SignInFormProps) {

FILE: components/spinner/spinner.tsx
  function Spinner (line 3) | function Spinner() {

FILE: components/ui/back-button.tsx
  type BackButtonProps (line 10) | type BackButtonProps = {
  function BackButton (line 16) | function BackButton({

FILE: components/ui/badge.tsx
  type BadgeProps (line 22) | interface BadgeProps
  function Badge (line 26) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: components/ui/button.tsx
  type ButtonProps (line 53) | interface ButtonProps

FILE: components/ui/gradient-text.tsx
  type TextElement (line 17) | type TextElement = HTMLParagraphElement | HTMLHeadingElement | HTMLSpanE...
  type TextProps (line 19) | interface TextProps

FILE: components/ui/input.tsx
  type InputProps (line 5) | interface InputProps

FILE: components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({

FILE: components/ui/spotlight-button.tsx
  type Props (line 6) | type Props = {
  function SpotlightButton (line 14) | function SpotlightButton({

FILE: components/ui/text-area.tsx
  type TextareaProps (line 5) | interface TextareaProps

FILE: components/ui/text.tsx
  type TextElement (line 24) | type TextElement = HTMLHeadingElement | HTMLParagraphElement
  type TextProps (line 26) | interface TextProps

FILE: components/ui/theme-button.tsx
  function ModeToggle (line 15) | function ModeToggle() {

FILE: components/ui/toast.tsx
  type ToastProps (line 112) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
  type ToastActionElement (line 114) | type ToastActionElement = React.ReactElement<typeof ToastAction>

FILE: components/ui/toaster.tsx
  function Toaster (line 13) | function Toaster() {

FILE: components/user-dropdown.tsx
  type MenuItem (line 22) | interface MenuItem {

FILE: hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher.ts
  function useAutomaticAspectRatioSwitcher (line 16) | function useAutomaticAspectRatioSwitcher({

FILE: hooks/canvas-area-hooks/use-resize-observer.ts
  function useCanvasResizeObserver (line 12) | function useCanvasResizeObserver(

FILE: hooks/canvas-area-hooks/use-screen-size-warning-toast.ts
  function useScreenSizeWarningToast (line 8) | function useScreenSizeWarningToast() {

FILE: hooks/use-editor.ts
  function useTiptapEditor (line 6) | function useTiptapEditor() {

FILE: hooks/use-event-listener.ts
  function useEventListener (line 40) | function useEventListener<

FILE: hooks/use-media-query.ts
  function useMediaQuery (line 4) | function useMediaQuery(query: string): boolean {

FILE: hooks/use-on-click-outside.ts
  type Handler (line 5) | type Handler = (event: MouseEvent) => void
  function useOnClickOutside (line 7) | function useOnClickOutside<T extends HTMLElement = HTMLElement>(

FILE: hooks/use-toast.ts
  constant TOAST_LIMIT (line 6) | const TOAST_LIMIT = 1
  constant TOAST_REMOVE_DELAY (line 7) | const TOAST_REMOVE_DELAY = 1000000
  type ToasterToast (line 9) | type ToasterToast = ToastProps & {
  function genId (line 25) | function genId() {
  type ActionType (line 30) | type ActionType = typeof actionTypes
  type Action (line 32) | type Action =
  type State (line 50) | interface State {
  function dispatch (line 131) | function dispatch(action: Action) {
  type Toast (line 138) | type Toast = Omit<ToasterToast, 'id'>
  function toast (line 140) | function toast({ ...props }: Toast) {
  function useToast (line 169) | function useToast() {

FILE: index.d.ts
  type Maybe (line 4) | type Maybe = null | undefined
  class ColorThief (line 23) | class ColorThief {

FILE: providers/index.tsx
  type ProviderProps (line 8) | type ProviderProps = {
  function Providers (line 12) | function Providers({ children }: ProviderProps) {

FILE: store/use-active-index.ts
  type ActiveIndexState (line 3) | interface ActiveIndexState {

FILE: store/use-background-options.ts
  type BackgroundOptionsState (line 3) | interface BackgroundOptionsState {

FILE: store/use-color-extractor.ts
  type ActiveIndexState (line 3) | interface ActiveIndexState {

FILE: store/use-frame-options.ts
  type FrameTypes (line 3) | type FrameTypes =
  type FrameOptionsState (line 10) | interface FrameOptionsState {

FILE: store/use-image-options.ts
  type ImageStyle (line 6) | interface ImageStyle {
  type ImageItem (line 29) | interface ImageItem {
  type ImageOptionsState (line 42) | interface ImageOptionsState {
  type SelectedLayerState (line 257) | interface SelectedLayerState {

FILE: store/use-image-quality.ts
  type ImageQualityState (line 3) | interface ImageQualityState {

FILE: store/use-moveable.ts
  type MoveableState (line 3) | interface MoveableState {

FILE: store/use-resize-canvas.ts
  type ResizeCanvasState (line 3) | interface ResizeCanvasState {

FILE: store/use-tiptap.ts
  type TiptapState (line 3) | interface TiptapState {

FILE: utils/auth-options.ts
  type Session (line 12) | interface Session extends DefaultSession {
  type JWT (line 22) | interface JWT {
  method jwt (line 46) | async jwt({ token, user, session, trigger }) {
  method session (line 87) | async session({ session, token }) {

FILE: utils/button-utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: utils/helper-fns.ts
  function splitWidthHeight (line 1) | function splitWidthHeight(resolution: string) {
  function convertHexToRgba (line 6) | function convertHexToRgba(hexCode: string, opacity = 1) {
  function calculateEqualCanvasSize (line 25) | function calculateEqualCanvasSize(
  function capitalize (line 44) | function capitalize(str: string) {
  function formatDate (line 89) | function formatDate(date: Date | string | number | undefined): string {
  function generateFormattedBlogDate (line 104) | function generateFormattedBlogDate(

FILE: utils/presets/gradients.ts
  type Gradient (line 1) | interface Gradient {

FILE: utils/presets/qualities.ts
  type Quality (line 1) | interface Quality {

FILE: utils/presets/resolutions.ts
  type Resolution (line 1) | interface Resolution {

FILE: utils/presets/shadows.ts
  type Shadow (line 1) | interface Shadow {
Condensed preview — 155 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (448K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 388,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 498,
    "preview": "{\n  \"tailwindCSS.experimental.classRegex\": [\n    [\"cva\\\\(([^)]*)\\\\)\", \"[\\\"'`]([^\\\"'`]*).*?[\\\"'`]\"],\n    [\"cx\\\\(([^)]*)\\\\"
  },
  {
    "path": "README.md",
    "chars": 832,
    "preview": "## Prismify\n\nPrismify is a web app that aims to revitalize & enhance boring images/screenshots. With prismify, you can e"
  },
  {
    "path": "app/(routes)/about/page.tsx",
    "chars": 5311,
    "preview": "/* eslint-disable react/no-unescaped-entities */\nimport React from 'react'\nimport BackButton from '@/components/ui/back-"
  },
  {
    "path": "app/(routes)/articles/[slug]/loading.tsx",
    "chars": 100,
    "preview": "import Loader from '@/components/loader'\n\nexport default function Loading() {\n  return <Loader />\n}\n"
  },
  {
    "path": "app/(routes)/articles/[slug]/page.tsx",
    "chars": 4877,
    "preview": "import BackButton from '@/components/ui/back-button'\nimport { Badge } from '@/components/ui/badge'\nimport { Text } from "
  },
  {
    "path": "app/(routes)/articles/page.tsx",
    "chars": 2975,
    "preview": "import prismadb from '@/libs/prismadb'\nimport { Text } from '@/components/ui/text'\nimport ArticleCard from '@/components"
  },
  {
    "path": "app/(routes)/articles/sitemap.ts",
    "chars": 526,
    "preview": "import prismadb from '@/libs/prismadb'\nimport { MetadataRoute } from 'next'\n\nexport default async function sitemap(): Pr"
  },
  {
    "path": "app/(routes)/layout.tsx",
    "chars": 271,
    "preview": "import Footer from '@/components/footer'\nimport React from 'react'\n\nexport default function AdminLayout({\n  children,\n}:"
  },
  {
    "path": "app/admin/layout.tsx",
    "chars": 208,
    "preview": "import React from 'react'\n\nexport default function AdminLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  ret"
  },
  {
    "path": "app/admin/write-article/blog-form.tsx",
    "chars": 13179,
    "preview": "'use client'\n\nimport { EditorProvider, useCurrentEditor } from '@tiptap/react'\nimport {\n  Bold,\n  Code,\n  Heading,\n  Ima"
  },
  {
    "path": "app/admin/write-article/page.tsx",
    "chars": 570,
    "preview": "import Loader from '@/components/loader'\nimport BackButton from '@/components/ui/back-button'\nimport dynamic from 'next/"
  },
  {
    "path": "app/api/article/post/route.ts",
    "chars": 1311,
    "preview": "import { z } from 'zod'\nimport { NextResponse } from 'next/server'\nimport prismadb from '@/libs/prismadb'\nimport { getCu"
  },
  {
    "path": "app/api/auth/[...nextauth]/route.ts",
    "chars": 206,
    "preview": "import { AuthOptions } from 'next-auth'\nimport { authOptions } from '@/utils/auth-options'\nimport NextAuth from 'next-au"
  },
  {
    "path": "app/api/user/settings/route.ts",
    "chars": 1754,
    "preview": "import { z } from 'zod'\nimport { NextResponse } from 'next/server'\nimport prismadb from '@/libs/prismadb'\nimport { getCu"
  },
  {
    "path": "app/error.tsx",
    "chars": 1671,
    "preview": "'use client'\n\n/* eslint-disable react/no-unescaped-entities */\nimport { Button, buttonVariants } from '@/components/ui/b"
  },
  {
    "path": "app/layout.tsx",
    "chars": 3779,
    "preview": "import ClarityScript from '@/components/clarity-script'\nimport Navbar from '@/components/navbar'\nimport { Toaster } from"
  },
  {
    "path": "app/loading.tsx",
    "chars": 100,
    "preview": "import Loader from '@/components/loader'\n\nexport default function Loading() {\n  return <Loader />\n}\n"
  },
  {
    "path": "app/manifest.json",
    "chars": 535,
    "preview": "{\n  \"name\": \"Prismify\",\n  \"short_name\": \"Prismify\",\n  \"start_url\": \"/\",\n  \"display\": \"standalone\",\n  \"background_color\":"
  },
  {
    "path": "app/not-found.tsx",
    "chars": 1221,
    "preview": "/* eslint-disable react/no-unescaped-entities */\n'use client'\n\nimport { buttonVariants } from '@/components/ui/button'\ni"
  },
  {
    "path": "app/page.tsx",
    "chars": 269,
    "preview": "import Sidebar from '@/components/editor/sidebar'\nimport Canvas from '@/components/editor/canvas-area'\n\nexport default f"
  },
  {
    "path": "app/robots.ts",
    "chars": 328,
    "preview": "import { MetadataRoute } from 'next'\n\nexport default function robots(): MetadataRoute.Robots {\n  return {\n    rules: {\n "
  },
  {
    "path": "app/sitemap.ts",
    "chars": 421,
    "preview": "import { MetadataRoute } from 'next'\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n  const sitemapUrls: Me"
  },
  {
    "path": "components/articles/article-card.tsx",
    "chars": 1297,
    "preview": "import Image from 'next/image'\nimport Link from 'next/link'\n\ntype ArticleCardProps = {\n  title: string\n  category: strin"
  },
  {
    "path": "components/auth-modal.tsx",
    "chars": 1609,
    "preview": "import * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { Dialog, DialogContent, DialogTr"
  },
  {
    "path": "components/clarity-script.tsx",
    "chars": 570,
    "preview": "import Script from 'next/script'\n\nconst ClarityScript = () => (\n  <Script id=\"clarity-script\" strategy=\"afterInteractive"
  },
  {
    "path": "components/color-picker.tsx",
    "chars": 1511,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport {\n  HexAlphaColorPicker,\n  HexColorInput,\n  HexColorPicker,\n} from"
  },
  {
    "path": "components/editor/background-image-canvas.tsx",
    "chars": 635,
    "preview": "// this component shows background image & noise in the canvas\n\nimport { useBackgroundOptions } from '@/store/use-backgr"
  },
  {
    "path": "components/editor/background-options/custom-gradient-picker.tsx",
    "chars": 3230,
    "preview": "'use client'\n\nimport { useCallback } from 'react'\nimport { solidColors } from '@/utils/presets/solid-colors'\nimport { Bu"
  },
  {
    "path": "components/editor/background-options/image-gradient-picker.tsx",
    "chars": 5583,
    "preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} "
  },
  {
    "path": "components/editor/background-options/index.tsx",
    "chars": 1745,
    "preview": "import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport { useBackgroundOptions } from '@/"
  },
  {
    "path": "components/editor/background-options/noise-slider.tsx",
    "chars": 1059,
    "preview": "import { Slider } from '@/components/ui/slider'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nim"
  },
  {
    "path": "components/editor/background-options/normal-gradient-picker.tsx",
    "chars": 16544,
    "preview": "'use client'\n\nimport { useState } from 'react'\n\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/"
  },
  {
    "path": "components/editor/background-options/pattern-picker.tsx",
    "chars": 5144,
    "preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} "
  },
  {
    "path": "components/editor/browser-frames.tsx",
    "chars": 3291,
    "preview": "// This component is responsible for rendering the browser frame around the image layer.\n\n'use client'\n\nimport { FrameTy"
  },
  {
    "path": "components/editor/canvas-area.tsx",
    "chars": 5985,
    "preview": "'use client'\n\nimport useAutomaticAspectRatioSwitcher from '@/hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher"
  },
  {
    "path": "components/editor/canvas-options/canvas-roundness-slider.tsx",
    "chars": 1549,
    "preview": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useResizeCanvas"
  },
  {
    "path": "components/editor/canvas-options/index.tsx",
    "chars": 5830,
    "preview": "'use client'\n\nimport {\n  Dribbble,\n  Facebook,\n  Instagram,\n  Linkedin,\n  Plus,\n  Minus,\n  Twitter,\n  Youtube,\n  ArrowRi"
  },
  {
    "path": "components/editor/canvas-options/resolution-button.tsx",
    "chars": 5943,
    "preview": "import { useState } from 'react'\nimport { cn } from '@/utils/button-utils'\nimport { useImageOptions, useSelectedLayers }"
  },
  {
    "path": "components/editor/frame-options/additional-frame-options.tsx",
    "chars": 4082,
    "preview": "import PopupColorPicker from '@/components/popup-color-picker'\nimport { Switch } from '@/components/ui/switch'\nimport { "
  },
  {
    "path": "components/editor/frame-options/frame-picker.tsx",
    "chars": 6471,
    "preview": "import {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport"
  },
  {
    "path": "components/editor/frame-options/index.tsx",
    "chars": 436,
    "preview": "import RoundnessOption from '../image-options/roundness-option'\nimport FramePicker from './frame-picker'\nimport Addition"
  },
  {
    "path": "components/editor/image-context-menu.tsx",
    "chars": 17243,
    "preview": "import { Button } from '@/components/ui/button'\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  Conte"
  },
  {
    "path": "components/editor/image-options/add-image-button.tsx",
    "chars": 4290,
    "preview": "import { ChangeEvent, useRef, useState } from 'react'\nimport { X, Plus, Upload } from 'lucide-react'\nimport { useImageOp"
  },
  {
    "path": "components/editor/image-options/index.tsx",
    "chars": 2287,
    "preview": "'use client'\n\nimport { Focus, GalleryVerticalEnd } from 'lucide-react'\nimport SizeOption from './scale-options'\nimport R"
  },
  {
    "path": "components/editor/image-options/inset-option.tsx",
    "chars": 4252,
    "preview": "'use client'\n\nimport { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/stor"
  },
  {
    "path": "components/editor/image-options/roundness-option.tsx",
    "chars": 2549,
    "preview": "'use client'\n\nimport { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/stor"
  },
  {
    "path": "components/editor/image-options/scale-options.tsx",
    "chars": 1413,
    "preview": "import { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-op"
  },
  {
    "path": "components/editor/image-options/shadow-settings.tsx",
    "chars": 7585,
    "preview": "'use client'\n\nimport PopupColorPicker from '@/components/popup-color-picker'\nimport { Button } from '@/components/ui/but"
  },
  {
    "path": "components/editor/main-image-area.tsx",
    "chars": 19622,
    "preview": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport { useOnClickOutside } from '@/hooks/use-on-click-out"
  },
  {
    "path": "components/editor/mobile-view-image-options.tsx",
    "chars": 1190,
    "preview": "import { ScrollArea } from '@/components/ui/scroll-area'\nimport BackgroundOptions from '@/components/editor/background-o"
  },
  {
    "path": "components/editor/moveable-component.tsx",
    "chars": 9078,
    "preview": "'use client'\n\nimport React from 'react'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\ni"
  },
  {
    "path": "components/editor/noise.tsx",
    "chars": 551,
    "preview": "import { useBackgroundOptions } from '@/store/use-background-options'\n\nexport default function Noise() {\n  const { noise"
  },
  {
    "path": "components/editor/perspective-options/index.tsx",
    "chars": 1926,
    "preview": "import { JoystickIcon, Rotate3d } from 'lucide-react'\nimport RotateOptions from './rotate-options'\nimport { Joystick } f"
  },
  {
    "path": "components/editor/perspective-options/rotate-options.tsx",
    "chars": 13307,
    "preview": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useMoveable } f"
  },
  {
    "path": "components/editor/position-options/index.tsx",
    "chars": 611,
    "preview": "'use client'\n\nimport SizeOption from '../image-options/scale-options'\nimport PositionControl from './position-control'\ni"
  },
  {
    "path": "components/editor/position-options/position-control.tsx",
    "chars": 4385,
    "preview": "'use client'\n\nimport {\n  CircleDot,\n  ArrowDown,\n  ArrowDownLeft,\n  ArrowDownRight,\n  ArrowLeft,\n  ArrowRight,\n  ArrowUp"
  },
  {
    "path": "components/editor/position-options/translate-control.tsx",
    "chars": 6888,
    "preview": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useMoveable } f"
  },
  {
    "path": "components/editor/selecto-component.tsx",
    "chars": 1370,
    "preview": "import React from 'react'\nimport { useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/s"
  },
  {
    "path": "components/editor/sidebar-buttons.tsx",
    "chars": 1035,
    "preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { useActiveIndexStore } from '@/store/use-active-in"
  },
  {
    "path": "components/editor/sidebar.tsx",
    "chars": 3768,
    "preview": "'use client'\n\nimport BackgroundOptions from '@/components/editor/background-options'\nimport { ScrollArea } from '@/compo"
  },
  {
    "path": "components/editor/text-context-menu.tsx",
    "chars": 3377,
    "preview": "import {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  Contex"
  },
  {
    "path": "components/editor/text-layers.tsx",
    "chars": 4800,
    "preview": "'use client'\n\nimport useTiptapEditor from '@/hooks/use-editor'\nimport { useImageOptions, useSelectedLayers } from '@/sto"
  },
  {
    "path": "components/editor/text-options/add-text-layer.tsx",
    "chars": 735,
    "preview": "'use client'\n\nimport { Plus } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { useImageOptio"
  },
  {
    "path": "components/editor/text-options/font-settings.tsx",
    "chars": 12797,
    "preview": "'use client'\n\nimport PopupColorPicker from '@/components/popup-color-picker'\nimport { Button } from '@/components/ui/but"
  },
  {
    "path": "components/editor/text-options/index.tsx",
    "chars": 673,
    "preview": "import { Type } from 'lucide-react'\nimport AddTextLayer from './add-text-layer'\nimport FontSettings from './font-setting"
  },
  {
    "path": "components/editor/tiptap-moveable.tsx",
    "chars": 3295,
    "preview": "'use client'\n\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useResizeCanvas } "
  },
  {
    "path": "components/editor/undo-redo-buttons.tsx",
    "chars": 1328,
    "preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { useTemporalStore } from '@/store/use-image-option"
  },
  {
    "path": "components/export-options.tsx",
    "chars": 8212,
    "preview": "'use client'\n\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { toast } "
  },
  {
    "path": "components/footer.tsx",
    "chars": 7285,
    "preview": "import { cn } from '@/utils/button-utils'\nimport Link from 'next/link'\nimport React from 'react'\nimport { buttonVariants"
  },
  {
    "path": "components/icons/index.tsx",
    "chars": 757,
    "preview": "import InfoIcon from './info.icon'\n\ntype IconType = {\n  name: 'info'\n  size?: number\n  color?: string\n  className?: stri"
  },
  {
    "path": "components/icons/info.icon.tsx",
    "chars": 2466,
    "preview": "import React from 'react'\n\nexport default function InfoIcon({\n  variant,\n}: {\n  variant?: 'default' | 'solid' | 'duotone"
  },
  {
    "path": "components/loader.tsx",
    "chars": 1799,
    "preview": "// Reusable loading spinner component to be used as suspense fallback on server components.\n\nexport default function Loa"
  },
  {
    "path": "components/navbar.tsx",
    "chars": 1451,
    "preview": "'use client'\n\nimport { AuthModal } from '@/components/auth-modal'\nimport ExportOptions from '@/components/export-options"
  },
  {
    "path": "components/navlinks.tsx",
    "chars": 3780,
    "preview": "// Links in the navbar\n\nimport { Badge } from '@/components/ui/badge'\nimport { buttonVariants } from '@/components/ui/bu"
  },
  {
    "path": "components/popup-color-picker.tsx",
    "chars": 1624,
    "preview": "// Formatted color picker component which opens a popover with a color picker.\n\nimport { Popover, PopoverContent, Popove"
  },
  {
    "path": "components/profile-dialog.tsx",
    "chars": 2470,
    "preview": "'use client'\n\nimport { useMemo } from 'react'\nimport { useSession } from 'next-auth/react'\nimport { User as UserIcon } f"
  },
  {
    "path": "components/settings-dialog.tsx",
    "chars": 5050,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useRouter } from 'next/navigation'\nimport {\n  Dialog,"
  },
  {
    "path": "components/sign-in-form.tsx",
    "chars": 3481,
    "preview": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport { useState } from 'react'\n\nimport { Button } from '@"
  },
  {
    "path": "components/spinner/spinner.module.css",
    "chars": 722,
    "preview": ".ring {\n  --uib-size: 25px;\n  --uib-speed: 2s;\n  --uib-color: #fefefe;\n  \n  height: var(--uib-size);\n  width: var(--uib-"
  },
  {
    "path": "components/spinner/spinner.tsx",
    "chars": 242,
    "preview": "import classes from './spinner.module.css'\n\nexport default function Spinner() {\n  return (\n    <>\n      <svg className={"
  },
  {
    "path": "components/ui/accordion.tsx",
    "chars": 2024,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { Ch"
  },
  {
    "path": "components/ui/back-button.tsx",
    "chars": 983,
    "preview": "'use client'\n\nimport React from 'react'\nimport { Text } from '@/components/ui/text'\nimport { Button } from '@/components"
  },
  {
    "path": "components/ui/badge.tsx",
    "chars": 1059,
    "preview": "import * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@/"
  },
  {
    "path": "components/ui/button.tsx",
    "chars": 3253,
    "preview": "import { cn } from '@/utils/button-utils'\nimport Spinner from '@/components/spinner/spinner'\nimport { VariantProps, cva "
  },
  {
    "path": "components/ui/circular-slider.tsx",
    "chars": 1116,
    "preview": "'use client'\n\nimport CircularSlider, {\n  CircularSliderProps,\n} from '@fseehawer/react-circular-slider'\nimport { useBack"
  },
  {
    "path": "components/ui/context-menu.tsx",
    "chars": 7269,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\"\nimport"
  },
  {
    "path": "components/ui/dialog.tsx",
    "chars": 4027,
    "preview": "// @ts-nocheck (for no reason build fails with this file)\n'use client'\n\nimport * as React from 'react'\nimport * as Dialo"
  },
  {
    "path": "components/ui/drawer.tsx",
    "chars": 3029,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport { Drawer as DrawerPrimitive } from 'vaul'\n\nimport { cn } from '@/uti"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "chars": 7300,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimpo"
  },
  {
    "path": "components/ui/gradient-text.tsx",
    "chars": 1114,
    "preview": "import { cn } from '@/utils/button-utils'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport { typ"
  },
  {
    "path": "components/ui/input.tsx",
    "chars": 818,
    "preview": "import * as React from 'react'\n\nimport { cn } from '@/utils/button-utils'\n\nexport interface InputProps\n  extends React.I"
  },
  {
    "path": "components/ui/popover.tsx",
    "chars": 1301,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\n\nimport { cn } "
  },
  {
    "path": "components/ui/scroll-area.tsx",
    "chars": 1656,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport "
  },
  {
    "path": "components/ui/select.tsx",
    "chars": 4439,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport * as SelectPrimitive from '@radix-ui/react-select'\nimport { Check, C"
  },
  {
    "path": "components/ui/separator.tsx",
    "chars": 779,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport * as SeparatorPrimitive from '@radix-ui/react-separator'\n\nimport { c"
  },
  {
    "path": "components/ui/skeleton.tsx",
    "chars": 270,
    "preview": "import { cn } from \"@/utils/button-utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivEl"
  },
  {
    "path": "components/ui/slider.tsx",
    "chars": 2066,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\n\nimport { cn } fr"
  },
  {
    "path": "components/ui/spotlight-button.tsx",
    "chars": 6893,
    "preview": "'use client'\n\nimport { cn } from '@/utils/button-utils'\nimport { Sparkles, Wand, Wand2 } from 'lucide-react'\n\ntype Props"
  },
  {
    "path": "components/ui/style/checkbox.module.css",
    "chars": 658,
    "preview": ".container {\n  display: inline-block;\n  vertical-align: middle;\n  margin-right: 10px;\n  cursor: pointer;\n  position: rel"
  },
  {
    "path": "components/ui/switch.tsx",
    "chars": 1212,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } f"
  },
  {
    "path": "components/ui/tabs.tsx",
    "chars": 1941,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \""
  },
  {
    "path": "components/ui/text-area.tsx",
    "chars": 741,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nexport interface TextareaProps\n  extends Reac"
  },
  {
    "path": "components/ui/text.tsx",
    "chars": 3088,
    "preview": "import { cn } from '@/utils/button-utils'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport { typ"
  },
  {
    "path": "components/ui/theme-button.tsx",
    "chars": 1454,
    "preview": "'use client'\n\nimport * as React from 'react'\nimport { MoonStar, SunMoon } from 'lucide-react'\nimport { useTheme } from '"
  },
  {
    "path": "components/ui/toast.tsx",
    "chars": 4817,
    "preview": "import * as React from 'react'\nimport * as ToastPrimitives from '@radix-ui/react-toast'\nimport { cva, type VariantProps "
  },
  {
    "path": "components/ui/toaster.tsx",
    "chars": 786,
    "preview": "'use client'\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from"
  },
  {
    "path": "components/ui/tooltip.tsx",
    "chars": 1168,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } "
  },
  {
    "path": "components/user-dropdown.tsx",
    "chars": 4952,
    "preview": "// dropdown menu for user profile when logged in\n\n'use client'\n\nimport { Button } from '@/components/ui/button'\nimport S"
  },
  {
    "path": "components.json",
    "chars": 334,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n"
  },
  {
    "path": "docker-compose.yml",
    "chars": 507,
    "preview": "version: '3.8'\n\nservices:\n  postgres:\n    image: postgres:15-alpine\n    container_name: prismify-db\n    restart: unless-"
  },
  {
    "path": "hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher.ts",
    "chars": 2479,
    "preview": "import { useColorExtractor } from '@/store/use-color-extractor'\nimport { useImageQualityStore } from '@/store/use-image-"
  },
  {
    "path": "hooks/canvas-area-hooks/use-resize-observer.ts",
    "chars": 1617,
    "preview": "\nimport { useImageQualityStore } from '@/store/use-image-quality'\nimport { useResizeCanvas } from '@/store/use-resize-ca"
  },
  {
    "path": "hooks/canvas-area-hooks/use-screen-size-warning-toast.ts",
    "chars": 634,
    "preview": "import React, { useEffect } from 'react'\nimport { toast } from '../use-toast'\n\n/**\n * This hook shows a warning toast if"
  },
  {
    "path": "hooks/use-editor.ts",
    "chars": 375,
    "preview": "import { useEditor } from '@tiptap/react'\nimport StarterKit from '@tiptap/starter-kit'\nimport { Color } from '@tiptap/ex"
  },
  {
    "path": "hooks/use-event-listener.ts",
    "chars": 2489,
    "preview": "import { RefObject, useEffect, useRef } from 'react'\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout"
  },
  {
    "path": "hooks/use-isomorphic-layout-effect.ts",
    "chars": 155,
    "preview": "import { useEffect, useLayoutEffect } from 'react'\n\nexport const useIsomorphicLayoutEffect =\n  typeof window !== 'undefi"
  },
  {
    "path": "hooks/use-media-query.ts",
    "chars": 1060,
    "preview": "\nimport { useEffect, useState } from 'react'\n\nexport function useMediaQuery(query: string): boolean {\n  const getMatches"
  },
  {
    "path": "hooks/use-on-click-outside.ts",
    "chars": 556,
    "preview": "import { RefObject } from 'react'\n\nimport { useEventListener } from './use-event-listener'\n\ntype Handler = (event: Mouse"
  },
  {
    "path": "hooks/use-toast.ts",
    "chars": 3922,
    "preview": "// Inspired by react-hot-toast library\nimport * as React from 'react'\n\nimport type { ToastActionElement, ToastProps } fr"
  },
  {
    "path": "index.d.ts",
    "chars": 1855,
    "preview": "/**\n * Represents a type that can be null or undefined.\n */\ntype Maybe = null | undefined\n\ndeclare module 'rgbaster' {\n "
  },
  {
    "path": "libs/prismadb.ts",
    "chars": 260,
    "preview": "import { PrismaClient } from '@prisma/client'\n\ndeclare global {\n  var prisma: PrismaClient | undefined\n}\n\nconst prismadb"
  },
  {
    "path": "libs/validators/article-post-validator.ts",
    "chars": 936,
    "preview": "import { z } from 'zod'\nimport sanitizeHtml from 'sanitize-html'\n\nexport const postSchema = z\n  .object({\n    title: z\n "
  },
  {
    "path": "libs/validators/user-settings-validator.ts",
    "chars": 211,
    "preview": "import { z } from 'zod'\n\nexport const userSettingsSchema = z.object({\n  name: z.string().min(1).max(50).optional().or(z."
  },
  {
    "path": "license.md",
    "chars": 546,
    "preview": "1. You are free to use Prismify for any purpose, commercial or non-commercial. While not required, I would appreciate it"
  },
  {
    "path": "middleware.ts",
    "chars": 292,
    "preview": "import { withAuth } from 'next-auth/middleware'\n\nexport default withAuth(\n  function middleware(req) {\n    console.log('"
  },
  {
    "path": "next.config.js",
    "chars": 819,
    "preview": "/** @type {import('next').NextConfig} */\n\nconst nextConfig = {\n  images: {\n    // domains: ['lh3.googleusercontent.com',"
  },
  {
    "path": "package.json",
    "chars": 3712,
    "preview": "{\n  \"name\": \"prismify\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"author\": {\n    \"name\": \"Silson\"\n  },\n  \"repository\":"
  },
  {
    "path": "postcss.config.js",
    "chars": 82,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "prettier.config.js",
    "chars": 126,
    "preview": "module.exports = {\n  plugins: [require(\"prettier-plugin-tailwindcss\")],\n  singleQuote: true,\n  semi: false,\n  tabWidth: "
  },
  {
    "path": "prisma/schema.prisma",
    "chars": 1816,
    "preview": "generator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider     = \"postgresql\"\n  url          = env"
  },
  {
    "path": "providers/index.tsx",
    "chars": 802,
    "preview": "'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { Provider } from 'react-w"
  },
  {
    "path": "store/use-active-index.ts",
    "chars": 282,
    "preview": "import { create } from 'zustand'\n\ninterface ActiveIndexState {\n  activeIndex: number\n  setActiveIndex: (index: number) ="
  },
  {
    "path": "store/use-background-options.ts",
    "chars": 1921,
    "preview": "import { create } from 'zustand'\n\ninterface BackgroundOptionsState {\n  background: string\n  setBackground: (background: "
  },
  {
    "path": "store/use-color-extractor.ts",
    "chars": 469,
    "preview": "import { create } from 'zustand'\n\ninterface ActiveIndexState {\n  extractedColor: {}\n  setExtractedColor: (extractedColor"
  },
  {
    "path": "store/use-frame-options.ts",
    "chars": 1547,
    "preview": "import { create } from 'zustand'\n\nexport type FrameTypes =\n  | 'Arc'\n  | 'MacOS Dark'\n  | 'MacOS Light'\n  | 'Shadow'\n  |"
  },
  {
    "path": "store/use-image-options.ts",
    "chars": 6915,
    "preview": "import { create, useStore } from 'zustand'\nimport { temporal, TemporalState } from 'zundo'\nimport throttle from 'just-th"
  },
  {
    "path": "store/use-image-quality.ts",
    "chars": 477,
    "preview": "import { create } from 'zustand'\n\ninterface ImageQualityState {\n  quality: number\n  setQuality: (quality: number) => voi"
  },
  {
    "path": "store/use-moveable.ts",
    "chars": 1023,
    "preview": "import { create } from 'zustand'\n\ninterface MoveableState {\n  showControls: boolean\n  setShowControls: (showControls: bo"
  },
  {
    "path": "store/use-resize-canvas.ts",
    "chars": 1471,
    "preview": "import { create } from 'zustand'\n\ninterface ResizeCanvasState {\n  resolution: string\n  setResolution: (res: string) => v"
  },
  {
    "path": "store/use-tiptap.ts",
    "chars": 268,
    "preview": "import { create } from 'zustand'\n\ninterface TiptapState {\n  shouldShow: boolean\n  setShouldShow: (shouldShow: boolean) ="
  },
  {
    "path": "styles/globals.css",
    "chars": 5674,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* A reusable h-dynamic-screen which has a fallback of vh if"
  },
  {
    "path": "tailwind.config.js",
    "chars": 2685,
    "preview": "const { fontFamily } = require('tailwindcss/defaultTheme')\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports ="
  },
  {
    "path": "tsconfig.json",
    "chars": 705,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  },
  {
    "path": "utils/auth-options.ts",
    "chars": 2857,
    "preview": "import { PrismaAdapter } from '@next-auth/prisma-adapter'\nimport {\n  type NextAuthOptions,\n  type DefaultSession,\n  getS"
  },
  {
    "path": "utils/button-utils.ts",
    "chars": 166,
    "preview": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: Cla"
  },
  {
    "path": "utils/helper-fns.ts",
    "chars": 3438,
    "preview": "export function splitWidthHeight(resolution: string) {\n  const [width, height] = resolution.split('x')\n  return { width,"
  },
  {
    "path": "utils/presets/gradients.ts",
    "chars": 7949,
    "preview": "export interface Gradient {\n  type: 'Normal' | 'Mesh'\n  gradient: string\n  background?: string\n}\n\nexport const gradients"
  },
  {
    "path": "utils/presets/qualities.ts",
    "chars": 240,
    "preview": "export interface Quality {\n  quality: string\n  value: number\n}\n\nexport const qualities = [\n  { quality: '0.5x Low', valu"
  },
  {
    "path": "utils/presets/resolutions.ts",
    "chars": 2169,
    "preview": "export interface Resolution {\n  name: string\n  resolutions?: Array<{\n    preset: string\n    resolution: string\n  }>\n  ic"
  },
  {
    "path": "utils/presets/shadows.ts",
    "chars": 825,
    "preview": "export interface Shadow {\n  name: string\n  fullName: string\n  shadow: string\n  preview: string\n}\n\nexport const shadows: "
  },
  {
    "path": "utils/presets/solid-colors.ts",
    "chars": 395,
    "preview": "export const solidColors = [\n  {\n    background: 'transparent',\n  },\n  {\n    background: '#cac2ff',\n  },\n  {\n    backgro"
  },
  {
    "path": "utils/tiptap-extensions.ts",
    "chars": 867,
    "preview": "import StarterKit from '@tiptap/starter-kit'\nimport Image from '@tiptap/extension-image'\nimport { Underline } from '@tip"
  },
  {
    "path": "workers/background-removal.worker.ts",
    "chars": 798,
    "preview": "self.onmessage = async (event: MessageEvent<{ src: string }>) => {\n  const { src } = event.data\n\n  if (!src) {\n    self."
  }
]

About this extraction

This page contains the full source code of the Sls0n/Prismify GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 155 files (412.6 KB), approximately 109.8k tokens, and a symbol index with 167 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!