[
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n.env*.local\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "MIT License\n\nCopyright (c) 2024 Mitul Shah\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<!-- This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -->\n\n[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/typicalmitul.svg?style=social&label=Follow%20%40typicalmitul&ref_src=twsrc%5Etfw)](https://twitter.com/typicalmitul)\n\nHi! My name is Mitul 🏄‍♂️ Welcome to the code that controls my evergrowing space on the internet. \n\nBuilding out my personal website has brought me a lot of joy over the years, and it's transformed many, many times. I actually try to update it every few weeks. \n\nIf you have any questions, suggestions – feel free to [tweet me](https://twitter.com/typicalmitul) 👋\n\n##### Built with\n* [Next.js](https://nextjs.org/)\n* [Vercel](https://vercel.com/)\n* [Tailwind CSS](https://tailwindcss.com/)\n* [Radix Primitives](https://radix-ui.com/)\n\n### Getting Started\n_Note: I do have an .env.example file, but it could take awhile to get those keys. You could delete all the code related to those endpoints if it makes things easier._\n\n```bash\n$ git clone https://github.com/mitul-s/$ mitul.ca.git\n$ cd mitul.ca\n$ npm install\n$ npm run dev\n# or yarn if you prefer that\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.\n\n---\n\n#### Deploy on Vercel \n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fmitul-s%2Fmitul.ca)\n"
  },
  {
    "path": "app/(with-layout)/about/page.tsx",
    "content": "import LinkPrimitive from \"@/components/link-primitive\";\nimport { beliefs, bucketList, Status } from \"@/content\";\nimport { ArrowLeft } from \"@phosphor-icons/react/dist/ssr/ArrowLeft\";\nimport { cva } from \"class-variance-authority\";\nimport Link from \"next/link\";\nimport type { Metadata } from \"next/types\";\nimport Section from \"@/components/section\";\n\nexport const metadata: Metadata = {\n  title: \"About\",\n  alternates: {\n    canonical: \"https://mitul.ca/about\",\n  },\n};\n\nconst bucketItem = cva([\"self-start\"], {\n  variants: {\n    status: {\n      none: \"\",\n      completed: [\"line-through\", \"text-gray-11\"],\n      progress: [\n        \"before:content-['']\",\n        \"before:w-1\",\n        \"before:h-1\",\n        \"before:bg-accent\",\n        \"before:inline-flex\",\n        \"before:-mt-px\",\n        \"before:rounded-full\",\n        \"before:animate-pulse\",\n        \"before:mr-1\",\n        \"flex\",\n        \"items-center\",\n      ],\n    },\n  },\n});\n\nconst BucketItem = ({\n  item,\n  status,\n}: {\n  item: string;\n  status: keyof typeof Status;\n}) => {\n  return <li className={bucketItem({ status: Status[status] })}>{item}</li>;\n};\n\nconst About = () => {\n  return (\n    <div className=\"justify-between md:flex animate-in fade-in duration-500\">\n      <div className=\"md:max-w-[450px] flex flex-col md:gap-y-0 gap-y-6 p-6\">\n        <Link\n          href=\"/\"\n          className=\"flex gap-x-1 bg-accent text-gray-1 w-fit rounded-sm pl-0.5 pr-1 py-0.5 leading-none items-center hover:bg-accent/50 transition duration-100 mx-1 md:mx-4\"\n          aria-label=\"Back\"\n        >\n          <ArrowLeft size={16} className=\"shrink-0\" />\n          <span className=\"text-sm font-medium\">Home</span>\n        </Link>\n        <Section heading=\"I'm still figuring it out\">\n          <div className=\"space-y-4\">\n            <p>\n              Hey, my name is Mitul and welcome to my space on the internet. I'm\n              a self-taught{\" \"}\n              <LinkPrimitive\n                href=\"https://bradfrost.com/blog/post/frontend-design/\"\n                external\n              >\n                design engineer\n              </LinkPrimitive>{\" \"}\n              based in Toronto, Canada.\n            </p>\n            <p>\n              Learning to code has felt like a superpower for me, it allows me\n              to bring any idea I can imagine to life. I love creating and\n              focusing on the little things that enhance our experiences as we\n              dive into the abyss of the web.\n            </p>\n            <p>\n              Apart from all of that, a strong sense of curiosity about the\n              world has always driven me. Travel, and specifically the diverse\n              experiences gained from exploring different places, cultures, and\n              landscapes, have significantly influenced my personal growth.\n            </p>\n            <p>\n              My life thrives on both chaos and serendipity. I'm just tryna\n              channel the same spirit of adventure as Ferris Bueller.\n            </p>\n          </div>\n        </Section>\n\n        <Section heading=\"Beliefs\">\n          <ul className=\"flex flex-col gap-y-1\">\n            {beliefs.map((belief) => {\n              return <li key={belief}>{belief}</li>;\n            })}\n          </ul>\n        </Section>\n        <Section heading=\"Bucket List\">\n          <ul className=\"flex flex-col gap-y-1\">\n            {bucketList.map((item) => {\n              return (\n                <BucketItem\n                  key={item.item}\n                  item={item.item}\n                  status={item.status}\n                />\n              );\n            })}\n          </ul>\n        </Section>\n      </div>\n    </div>\n  );\n};\n\nexport default About;\n"
  },
  {
    "path": "app/(with-layout)/layout.tsx",
    "content": "import type { Viewport } from \"next\";\n\nexport const viewport: Viewport = {\n  themeColor: \"#0F0F0F\",\n};\n\nconst Layout = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <div className=\"h-full relative\">\n      <div className=\"main-noise\" aria-hidden />\n      {children}\n    </div>\n  );\n};\nexport default Layout;\n"
  },
  {
    "path": "app/(with-layout)/page.client.tsx",
    "content": "\"use client\";\n\nimport { Globe, Terminal } from \"@phosphor-icons/react\";\nimport { track } from \"@vercel/analytics\";\nimport Link from \"next/link\";\n\ninterface ProjectProps {\n  title: string;\n  description: string;\n  hrefs: {\n    live?: string;\n    code?: string;\n  };\n}\n\nconst Project = ({\n  title,\n  description,\n  hrefs: { live, code },\n}: ProjectProps) => {\n  return (\n    <div className=\"px-4 pt-4 pb-5 flex flex-col gap-y-1\">\n      <h3 className=\"font-medium\">{title}</h3>\n      <p>{description}</p>\n      <div className=\"flex items-center mt-2 gap-x-2\">\n        {live ? (\n          <Link\n            className=\"flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm\"\n            href={live}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            onClick={() => track(\"project_live_link_clicked\", { title })}\n          >\n            <Globe\n              aria-hidden={true}\n              size={12}\n              className=\"shrink-0 text-gray-1\"\n            />\n            Live{\" \"}\n          </Link>\n        ) : null}\n        {code ? (\n          <Link\n            className=\"flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm\"\n            href={code}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            onClick={() => track(\"project_code_link_clicked\", { title })}\n          >\n            <Terminal\n              aria-hidden={true}\n              size={12}\n              className=\"shrink-0 text-gray-1\"\n            />\n            Code{\" \"}\n          </Link>\n        ) : null}\n      </div>\n    </div>\n  );\n};\n\nexport { Project };\n"
  },
  {
    "path": "app/(with-layout)/page.tsx",
    "content": "import { Accordion, AccordionItem } from \"@/components/collapsible\";\nimport VideoHoverPreview from \"@/components/video-hover-preview\";\nimport { experiences, photos } from \"@/content\";\nimport { ArrowRight } from \"@phosphor-icons/react/dist/ssr/ArrowRight\";\nimport Link from \"next/link\";\nimport Gallery from \"@/components/gallery\";\nimport { PencilSimpleLine } from \"@phosphor-icons/react/dist/ssr/PencilSimpleLine\";\nimport TwitterXMotion from \"@/components/twitter-x-loop\";\nimport {\n  CopyEmailButton,\n  CopyEmailButtonAlt,\n} from \"@/components/copy-email-button\";\nimport Footer from \"@/components/footer\";\nimport { cn } from \"@/lib/utils\";\nimport { ScribbleLoop } from \"@phosphor-icons/react/dist/ssr/ScribbleLoop\";\nimport MusicPlayer from \"@/components/music-player\";\nimport Shader from \"@/components/shader\";\nimport { Project } from \"./page.client\";\nimport VideoPauseButton from \"@/components/video-pause-button\";\n\nconst DottedSpacer = ({\n  lines = 3,\n  className,\n}: {\n  lines?: number;\n  className?: string;\n}) => {\n  return (\n    <div className={cn(\"flex gap-y-0.5 my-0.5 flex-col\", className)}>\n      {Array.from({ length: lines }).map((_, index) => (\n        <span\n          // biome-ignore lint/suspicious/noArrayIndexKey: this is a static list\n          key={index}\n          className=\"h-px border-b border-dotted border-accent\"\n          aria-hidden\n        />\n      ))}\n    </div>\n  );\n};\n\nconst SectionTitle = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <h2 className=\"md:py-4 pt-4 pl-4 md:pr-4 font-medium h-full md:ml-auto\">\n      {children}\n    </h2>\n  );\n};\n\nconst Section = ({\n  title,\n  children,\n}: {\n  title?: string;\n  children: React.ReactNode;\n}) => {\n  return (\n    <section className=\"md:grid grid-cols-[160px_500px_auto] md:divide-x w-full border-b border-accent divide-accent text-accent\">\n      {title ? <SectionTitle>{title}</SectionTitle> : null}\n      <SectionContent>{children}</SectionContent>\n    </section>\n  );\n};\n\nconst SocialLink = ({\n  href,\n  social,\n  children,\n}: {\n  href: string;\n  social: string;\n  children: React.ReactNode;\n}) => {\n  return (\n    <div className=\"grid grid-cols-[75px_auto_auto] gap-x-1 items-center px-4 py-2\">\n      <p className=\"font-medium\">{social}</p>\n      <Link\n        href={href}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"hover:underline underline-offset-2\"\n      >\n        {children}\n      </Link>\n    </div>\n  );\n};\n\nconst SectionContent = ({ children }: { children: React.ReactNode }) => {\n  return <div className=\"md:border-r\">{children}</div>;\n};\n\nexport default function Home() {\n  return (\n    <>\n      <Shader />\n      <div className=\"justify-between md:flex animate-in fade-in duration-500 select flex-col\">\n        <nav className=\"min-md:absolute top-4 right-4 flex max-md:p-4 gap-1 text-accent font-medium\">\n          <Link\n            href=\"/os\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer\"\n          >\n            Public Archive\n          </Link>\n          <Link\n            href=\"/visitors\"\n            className=\"flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer\"\n          >\n            Guestbook\n          </Link>\n          <CopyEmailButtonAlt />\n          <VideoPauseButton />\n          {/* <ThemeChanger /> */}\n        </nav>\n\n        <Section title=\" \">\n          <div className=\"px-4 min-md:pt-8 pb-6 col-start-2\">\n            <h1 className=\"font-medium flex items-center gap-x-1.5 text-[24px]\">\n              Mitul Shah\n            </h1>\n            <span>Photographer, design engineer, and a bit more.</span>\n            <p className=\"mt-4\">\n              Crafting memorable interfaces with a deep attention to detail. I\n              dedicate most my time to continuous learning and refining my\n              skillset.\n            </p>\n            <p className=\"mt-2\">\n              I'm a creative{\" \"}\n              <VideoHoverPreview href=\"https://www.youtube.com/watch?v=jG7dSXcfVqE\">\n                doing what I can't\n              </VideoHoverPreview>\n            </p>\n            <span className=\"font-medium text-sm tracking-tight mt-4 -mb-2 block\">\n              Currently\n            </span>\n            <MusicPlayer />\n            <div className=\"flex gap-x-2\">\n              <Link\n                href=\"/visitors\"\n                className=\"rounded-4 bg-accent transition hover:bg-accent/90 text-light-green font-medium px-2 py-1 flex gap-x-1.5 items-center\"\n                style={{\n                  boxShadow:\n                    \"0 4px 4px #08080814, 0 1px 2px #08080833, inset 0 6px 12px #ffffff1f, inset 0 1px 1px #fff3\",\n                }}\n              >\n                Sign guestbook\n                <ScribbleLoop\n                  size={12}\n                  weight=\"bold\"\n                  aria-hidden={true}\n                  className=\"shrink-0 text-light-green\"\n                />\n              </Link>\n            </div>\n          </div>\n        </Section>\n        <Section title=\"Experience\">\n          <DottedSpacer className=\"mt-4 md:mt-0.5 mb-0\" />\n          <Accordion className=\"h-full flex flex-col divide-y divide-dotted\">\n            {experiences.map((role) => {\n              return (\n                <AccordionItem\n                  key={role.company}\n                  role={role.role}\n                  company={role.company}\n                  range={role.range}\n                  description={role.description}\n                  // skills={role.skills}\n                  link={role.link}\n                />\n              );\n            })}\n            <DottedSpacer lines={2} />\n          </Accordion>\n        </Section>\n        <Section title=\"Projects\">\n          <Project\n            title=\"Daybloom\"\n            description=\"A mindful daily journal that combines your photos and thoughts into calendar view, helping you capture memories and reflect on life's meaningful moments.\"\n            hrefs={{\n              live: \"https://daybloom.app\",\n            }}\n          />\n          <DottedSpacer className=\"my-0\" />\n          <Project\n            title=\"Montreal in Motion\"\n            description=\"A documentation of the brutalist and distinctly designed metro stations. The project uses CSS 3D transforms and noise to mirror the architecutral character of the spaces.\"\n            hrefs={{\n              live: \"https://typicalmitul.com/montreal-in-motion\",\n              code: \"https://github.com/mitul-s/typicalmitul.com\",\n            }}\n          />\n          <DottedSpacer className=\"my-0\" />\n          <Project\n            title=\"Places to Read\"\n            description=\"A microsite to discover community submitted parks around the world where you can sit down, chill and enjoy reading a book.\"\n            hrefs={{ live: \"https://placestoread.xyz\" }}\n          />\n        </Section>\n        <Section title=\"Photography\">\n          <div className=\"flex flex-col gap-y-1.5 pt-4 pb-8\">\n            <div className=\"flex flex-col gap-y-1.5 px-4\">\n              <p>\n                I've built up my craft as a photographer over a number of years\n                and thrived in turning it into an indepedent business.\n              </p>\n              <span>\n                <span className=\"font-medium\">\n                  Notable achievements include\n                </span>\n                <ul>\n                  <li className=\"relative flex items-center before:w-1 before:h-1 before:bg-accent before:rounded-full before:leading-none gap-x-2 \">\n                    being a personal photographer for the Uber CEO\n                  </li>\n                  <li className=\"relative flex items-center before:w-1 before:h-1 before:bg-accent before:rounded-full before:leading-none gap-x-2 \">\n                    featured in local Toronto newspapers\n                  </li>\n                  <li className=\"relative flex items-center before:w-1 before:h-1 before:bg-accent before:rounded-full before:leading-none gap-x-2 \">\n                    having a photo as a wallpaper in every Google device\n                  </li>\n                </ul>\n              </span>\n              <p>\n                Today, my focus is on music photography where I capture my\n                favourite artists at concerts or festivals. You can learn a\n                little more by visiting my portfolio below.\n              </p>\n              <Link\n                href=\"https://typicalmitul.com\"\n                target=\"_blank\"\n                className=\"flex w-fit gap-x-2 items-center transition hover:bg-accent/90 rounded-4 bg-accent text-light-green font-medium px-2 py-1 mt-2 mb-4\"\n                style={{\n                  boxShadow:\n                    \"0 4px 4px #08080814, 0 1px 2px #08080833, inset 0 6px 12px #ffffff1f, inset 0 1px 1px #fff3\",\n                }}\n              >\n                Visit my portfolio\n                <ArrowRight size={12} aria-hidden={true} />\n              </Link>\n            </div>\n            <div className=\"scroll-pl-4 scroll-pr-4\">\n              <Gallery photos={photos} />\n            </div>\n          </div>\n        </Section>\n        <Section title=\"Contact\">\n          <p className=\"pb-4 px-4 pt-4\">\n            Let's hang, up for a chat or just say hi. If you're into\n            photography, film or music, I'd love to hear from you.\n          </p>\n          <DottedSpacer lines={2} />\n          <div className=\"grid gap-x-4 divide-y border-y divide-dotted border-dotted\">\n            <div className=\"grid grid-cols-[75px_auto_1fr] gap-x-1.5 items-center px-4 py-2\">\n              <p className=\"font-medium\">Mail</p>\n              <Link href=\"mailto:mitulxshah@gmail.com\">\n                mitulxshah@gmail.com\n              </Link>\n              <div className=\"flex gap-x-1 ml-auto\">\n                <Link\n                  href=\"mailto:mitulxshah@gmail.com\"\n                  className=\"hidden min-md:flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm w-fit\"\n                >\n                  <PencilSimpleLine size={12} aria-hidden={true} />\n                  Compose\n                </Link>\n                <CopyEmailButton />\n              </div>\n            </div>\n            <TwitterXMotion className=\"grid grid-cols-[75px_auto_1fr] gap-x-1.5 items-center px-4 py-2 overflow-hidden\" />\n            <SocialLink\n              social=\"Instagram\"\n              href=\"https://instagram.com/typicalmitul\"\n            >\n              @typicalmitul\n            </SocialLink>\n            <SocialLink social=\"GitHub\" href=\"https://github.com/mitul-s\">\n              mitul-s\n            </SocialLink>\n          </div>\n          <DottedSpacer lines={2} />\n        </Section>\n\n        <Footer />\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/(without-root-layout)/layout.tsx",
    "content": "const Layout = ({ children }: { children: React.ReactNode }) => {\n  return <>{children}</>;\n};\n\nexport default Layout;\n"
  },
  {
    "path": "app/(without-root-layout)/p/2024/page.mdx",
    "content": "import Smol from \"./smol-txt\";\nimport Figure from \"./../components/figure\";\nimport { BlogPostJsonLd } from \"@/components/json-ld\";\n\nimport tropez from \"./tropez.jpeg\";\nimport timechamber from \"./timechamber.jpeg\";\n\nexport const metadata = {\n  title: \"[Annual Review] 2024\",\n  description: \"Everything I want.\",\n  authors: [{ name: \"Mitul Shah\", url: \"https://mitul.ca\" }],\n  alternates: {\n    canonical: \"/p/2024\",\n  },\n  openGraph: {\n    type: \"article\",\n    title: \"[Annual Review] 2024 / Mitul Shah\",\n    description: \"Everything I want.\",\n    publishedTime: \"2024-12-31\",\n    authors: [\"Mitul Shah\"],\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"[Annual Review] 2024 / Mitul Shah\",\n    description: \"Everything I want.\",\n  },\n};\n\n<BlogPostJsonLd slug=\"2024\" />\n\n# [Annual Review] 2024\n\n<Figure\n  src={tropez}\n  alt=\"Saint-Tropez, Maximilien Luce – 1893\"\n  caption=\"Saint-Tropez, Maximilien Luce – 1893\"\n/>\n\nI know I say it all the time, but every year is the best year of my life. I will always do my best to achieve everything I set out to.\n\nThe past few years were dedicated to exploration and understanding — 2024 allowed me to reap the rewards of that journey. I can't pinpoint exactly what shifted, maybe it doesn't matter, but for the first time, everything feels right.\n\nI feel in control.\n\nThis clarity has reflected positively in all my relationships; with friends, love, and family. Building an understanding of what you want and what you will tolerate makes things easier for both you and everyone around you.\n\n## Goals\n\nComparing my [2023](https://futureland.tv/@mitul/%E2%9C%A6-2023-%E2%9C%A6) journal to [2024](https://futureland.tv/@mitul/%E2%88%97-2024-%E2%88%97), it's obvious there's nearly no structure lol. This was intentional as I only had two primary goals for the year. For the smaller ones, things didn't always go as planned, but I'm okay with that.\n\n### Code\n\nMy creative aspirations this year kind of ended up on a back burner. While I initially set out to complete six projects; it was not a priority as I was traveling. I managed to bring three and a half projects to life which I'm super proud of.\n\n- [Montreal in Motion](https://typicalmitul.com/montreal-in-motion)\n- [Guestbook](/visitors)\n- A website for a friend and a hackathon project\n\nI have a massive project that's close to completion, but it's been ongoing for so long that I just can't get myself back into it. I'll keep trying.\n\nAn unexpected highlight was that [Places to Read](https://placestoread.xyz) went viral through an Instagram reel. The response was overwhelming – over 300 submissions from strangers across the globe, each sharing their own favourite reading spaces. It was a good reminder of why I believe being able to code is a superpower.\n\n### Photography\n\nMy concert photography declined significantly this year, mainly because I felt burnt out. I shot fewer than 10 shows this year—I haven't kept exact count. While it's personally fulfilling, it offers few tangible benefits right now.\n\nIn contrast, I photographed +40 acts last year including festivals and didn't make a dime (not that $ is the priority). It's tough.\n\nI'm at a crossroads: though I'm confident in my skills, I'm unsure how to grow this work into something bigger.\n\n### Cooking and relationships\n\nI love cooking for people. I hosted a potluck, partly to see if I could make food people enjoy.\n\nMore importantly, there was something deeply satisfying about creating a space where different friend groups could come together over a shared meal, each person bringing their own piece of themselves to the table.\n\n## Highlights\n\n### New Demos 1\n\nThe year started off with [New Demos](https://www.newdemos.ca/) 1, where I got the opportunity to present my project [Montreal in Motion](https://typicalmitul.com/montreal-in-motion).\n\nThe night after, I landed a contract gig that funded my travels.\n\nND1 kicked off a domino effect in Toronto that brought 1,000s of people together for the sake of progress and growth. It also sparked countless new meaningful friendships which shaped my summer back home after my travels.\n\nI'm forever grateful to [internetVin](https://x.com/internetvin) and [Tommy Trinh](https://x.com/tommytrxnh) for what they've done for the city of Toronto.\n\n### The Last Rodeo: Four months of travel\n\nTravel has been a core part of my life for the past two years and I wanted to do one last, long backpacking trip, specifically one where I wasn't tied to a job or major responsibilities that affected how I travelled. I don't know if I'll ever do this again, but I'm so glad I did it one last time.\n\n- London\n- Turkey (Istanbul, Antalya, Cappadocia, back to Istanbul)\n- [Bosnia and Herzegovina](https://x.com/typicalmitul/status/1777433751737258475) (Sarjevo, Mostar)\n- Crotia (Split, Zagreb)\n- Slovenia (Ljubljana, Lake Bled)\n- Budapest\n- Prague\n- 6 weeks in Spain (Barcelona, Valencia, Alicante, Malaga, Granada, Madrid, Barcelona again and back to Madrid, Seville, Cadiz, Tarifa)\n- Morocco via boat from Spain (Tangier, Chefchaouen, Fes, Casablanca, Essaouira, nine nights in Taghazout)\n- Porto, Portugal to end my travels\n\nPart of me still misses travelling, though I do recognize it as a form of escapism. While I'll always deeply cherish the lifelong friends and memories I've made, I'm glad to be back home. I mean, I can't lie, it's pretty cool I can randomly bump into travel friends in so many places of the world.\n\nI missed out on a lot in Toronto, and it was a trade-off I was willing to make. I knew this trip was something I had to do.\n\nI could write a million words on my adventures, but that would be cliche. The overall goal was simple: travel until I land a full time job.\n\n### Vercel\n\nWhile travelling, I landed a role at [Vercel](https://x.com/typicalmitul/status/1812914498619253065) as a Design Engineer. This was a dream company and I'm grateful to be here. It's an absolute honour to be working alongside the people I've always looked up to.\n\nNext.js changed my life, and that's probably an understatement.\n\n## 2025\n\nA goal I've been trying to achieve for nearly two years is almost in fruition. That's my main priority for the next little bit.\n\nEvery year is quite significantly different from the last, but 2025 is when I introduce stability into my life and I am excited for that.\n\n<Figure\n  src={timechamber}\n  alt=\"Hyperbolic Time Chamber from Dragonball Z\"\n  caption=\"One year inside the chamber equates to just one day in the outside world. The air grows denser, and the temperature fluctuates more intensely the deeper one goes into the training area.\"\n/>\n\n---\n\n<Smol />\n"
  },
  {
    "path": "app/(without-root-layout)/p/2024/smol-txt.tsx",
    "content": "import Link from \"next/link\";\nimport React from \"react\";\n\nconst Smol = () => {\n  return (\n    <p className=\"text-gray-11 text-[12px]\">\n      Previous years [\n      <Link\n        className=\"text-blue-9 hover:text-blue-10 hover:underline\"\n        href=\"https://futureland.tv/@mitul/entry/385302\"\n        target=\"_blank\"\n        rel=\"noreferrer noopener\"\n      >\n        2023\n      </Link>\n      ]\n    </p>\n  );\n};\n\nexport default Smol;\n"
  },
  {
    "path": "app/(without-root-layout)/p/2025/page.mdx",
    "content": "import Smol from \"./smol-txt\";\nimport Figure from \"./../components/figure\";\nimport ImageContainer from \"../components/img-container\";\nimport Callout from \"../components/callout\";\nimport monet from \"./opengraph-image.jpg\";\nimport { BlogPostJsonLd } from \"@/components/json-ld\";\n\nexport const metadata = {\n  title: \"[Annual Review] 2025\",\n  description: \"A year in review: moving, adjusting, and finding breathing room\",\n  authors: [{ name: \"Mitul Shah\", url: \"https://mitul.ca\" }],\n  alternates: {\n    canonical: \"/p/2025\",\n  },\n  openGraph: {\n    type: \"article\",\n    title: \"[Annual Review] 2025 / Mitul Shah\",\n    description: \"A year in review: moving, adjusting, and finding breathing room\",\n    publishedTime: \"2025-12-31\",\n    authors: [\"Mitul Shah\"],\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"[Annual Review] 2025 / Mitul Shah\",\n    description: \"A year in review: moving, adjusting, and finding breathing room\",\n  },\n};\n\n<BlogPostJsonLd slug=\"2025\" />\n\n# [Annual Review] 2025\n\n<Figure\n  src={monet}\n  alt=\"The Seine at Lavacourt, Claude Monet – 1880\"\n  caption=\"The Seine at Lavacourt, Claude Monet – 1880\"\n/>\n\n<Callout>\nYou can also read this on Substack\n</Callout>\n\nI started the year in the middle of Buenos Aires watching fireworks with strangers and wondered why everyone was wearing white. I missed my flight home and reconsidered all my life decisions, but thankfully I had friends in the city that I could spend time with.\n\nI was living with the belief I would soon move to New York City once I was home, but little did I know, my drives to Detroit and Buffalo would end with rejections.\n\nI switched to a sublet on King St West and made a point to spend time with my friends while I waited for my visa approval, knowing they soon wouldn’t be a 10 minute walk from me. I spent most of my days at [New Stadium](https://x.com/newsystems_).\n\nBy March, my visa was approved and I got ready to move away from home for the first time.\n\nJune, I was in New York City. September, I signed a lease.\n\nI’ll be spending my new year in Guatemala and El Salvador, likely hiking, surfing, being on the beach and getting the time away that I sometimes find myself missing. Most often when my travel friends reach out reminding me of our memories together.\n\n<Figure src=\"https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/dbrand%20Twitter%20Post.png\" \nalt=\"Messages from travel friends on Instagram reminding me of our travels\" />\n\nI met [Shen](https://shen.land) in person this year and she's a great reminder to be more myself.\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_3948.PNG)\n</ImageContainer>\n\nI was the first guest on [Rudy’s podcast](https://open.spotify.com/episode/3ZFlTNlsLa8e2nYINUG0UF?si=73c3fe14627e4450).\n\n<Figure src=\"https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%282%29.jpg\" \nalt=\"Comments from people saying they enjoyed me on Rudy's podcast\" />\n\nI hosted weekly dinners. I made new friends. I lost new friends. I started working from an office again. I flew home to see the Blue Jays win (and my friends and family). The Blue Jays lost.\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_7821.jpg)\n</ImageContainer>\n\nThere’s a certain beauty in knowing I achieved the things I planned for this year. Write more – I mean, here you are. Take photos. Move to New York City. Get my first apartment. Travel less. I think I did it all.\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_7112.jpg)\n</ImageContainer>\n\nThough there is something disorienting about getting what you wanted. An old friend always used to ask me what’s next.\n\nAfter years of thinking about it, what’s next is not the point.\n\n--\n\nI went to Naples and watched them win the championship, my first time in Italy. With a need for stability in my life, I only travelled twice this year. Quite significantly less than the years prior. We’re probably going to pick that back up by a bit.\n\nI said goodbye to my friends. I still can’t believe Daniil cooked for everyone.\n\n<Figure src=\"https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/DSC02050.JPG\" \nalt=\"Group photo with most my friends, ironically missing Daniil\"\n/>\n\nLife is a never ending set of side quests. I don’t really like letting the mundane set in.\n\nFocus on building your Dad lore.\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%281%29.jpg)\n</ImageContainer>\n\n\nThere’s a lot this year I did but it primarily revolved around moving to a new city for the first time. I haven’t really figured things out here yet, but I finally feel like I have breathing room to get back to things I’d like to do. The side quests.\n\nI found my first apartment. Though it took awhile, it was nice to jump between Bushwick, Williamsburg and eventually settle into LES.\n\nI hosted a little Friendsgiving party; it wasn’t anything like the one I hosted back home in Toronto but it’s a learning process.\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_1834.jpeg)\n</ImageContainer>\n\nBeing in a new city hasn’t been the easiest; I miss the sense of familiarity more than anything. I am happier here, or happier overall. The reality, the texture of moving to a new big city and acknowledging what the changes actually feel like rather than what they’re supposed to feel like is a bit hard, but it’s part of the human experience.\n\nCan I make my life feel like an episode of Friends/Seinfeld/HIMYM? I’d bet so. It just takes time. It already feels like a loop of Ferris Bueller’s Day Off.\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%283%29.jpg)\n</ImageContainer>\n\n--\n\nI don't know who I am if I am not in a state of discomfort.\n\nIt’s more I feel like I’ve reached a position where I’m deeply familiar with the things I do love and enjoy, and have done a great job of instilling them in my life but yet lost touch with them as I played with new, unfamiliar things for the sake of curiosity. That’s probably the best way I can phrase it.\n\n\n<Figure src=\"https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%284%29.jpg\" alt=\"Pokemon Ruby Generation screen with a motivational quote\" />\n\nSo for the upcoming year, I want to be extremely disciplined. Here’s some examples of what that could include:\n\nWrite more. Let’s double or triple the amount of write ups I share publicly.\n\nNo more solo travelling. Either a) travel with friends, or b) meet travel friends in their respective countries. Solo travel did wonders for me, it helped me shape who I am. Travelling with friends is hard, there’s different levels of comfort, schedules, risk tolerance and such but the times where it all aligns, makes it all worth it.\n\nTake even more photos but print them. I told my friend the other day, I don’t want to forget what I looked like today. I want to capture myself, my friends and everything in meaningful ways - something more than a 0.5*. So when the time comes and I share my photos (with friends, my kids, my family), there’s a certain level of love thought attached to it.\n\nI got a 3D printer. I want to design something of my own.\n\nI’m going to pick up film photography again. I have been anti-film as every camera I’ve owned has broke, but I’ll remember to be more careful this year.\n\n---\n\nNever did I enjoy the beach much, but now I seem to be pulled towards it. I think a lot of the influence certain people have on my life, I changed because of them. I want to spend more time in the ocean, more time surfing, maybe learn to swim (better).\n\nAnyways. Everyday is an adventure, and it will continue to be.\n\nIf you haven’t noticed I’m bad at telling the “why”, I kind of don’t want to unless someone asks me explicitly. If you think we’d get along, or I can help you, or you can help me. Email me here [mitulxshah@gmail.com](mailto:mitulxshah@gmail.com).\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/IMG_1145.jpg)\n</ImageContainer>\n\n**2026**\n\nThough internalized, a reminder, whatever it takes.\n\n<ImageContainer>\n![.png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/p2025/Annual%20Review%202025%20Image%20%285%29.jpg)\n</ImageContainer>\n\n*****\n\nSee previous years [[2024](/p/2024), [2023](https://futureland.tv/@mitul/entry/385302)]\n\nSee the quarters for 2025 [[Q1](/p/q1-2025), [Q2](/p/q2-2025), [Q3](/p/q3-2025)]\n\n"
  },
  {
    "path": "app/(without-root-layout)/p/components/callout.tsx",
    "content": "\"use client\";\n\nimport { ArrowUpRight } from \"@phosphor-icons/react\";\nimport Link from \"next/link\";\n\nconst Callout = ({ children }: { children: React.ReactNode }) => {\n    return (\n    <Link href=\"https://fieldnotesbymitul.substack.com/p/annual-review-2025\" target=\"_blank\" rel=\"noopener noreferrer\" className=\"bg-accent text-gray-1 p-4 rounded-md flex items-center justify-between hover:bg-accent/80 transition\">\n      <p className=\"text-sm\">{children}</p>\n      <ArrowUpRight size={14} className=\"shrink-0\" />\n    </Link>\n  );\n};\n\nexport default Callout;"
  },
  {
    "path": "app/(without-root-layout)/p/components/figure.tsx",
    "content": "import Image, { type StaticImageData } from \"next/image\";\n\ninterface FigureProps {\n  src: string | StaticImageData;\n  alt: string;\n  caption?: string;\n  width?: number;\n  height?: number;\n}\n\nconst Figure = (props: FigureProps) => {\n  const { src, alt, caption, width, height } = props;\n  const isExternalUrl = typeof src === \"string\";\n\n  // For external URLs without dimensions, use unoptimized mode\n  const imageProps = isExternalUrl && !width\n    ? { unoptimized: true, width: 1400, height: 900 }\n    : width && height\n      ? { width, height }\n      : {};\n\n  if (caption !== undefined) {\n    return (\n      <figure>\n        <Image\n          className=\"rounded-4 border border-gray-6\"\n          src={src}\n          alt={alt}\n          fetchPriority=\"high\"\n          quality={30}\n          placeholder={isExternalUrl ? \"empty\" : \"blur\"}\n          sizes=\"(max-width: 768px) 100vw, 700px\"\n          {...imageProps}\n        />\n        <figcaption className=\"text-sm text-gray-11 mt-1.5\">\n          {caption}\n        </figcaption>\n      </figure>\n    );\n  }\n  return (\n    <Image\n      className=\"rounded-4 border border-gray-6\"\n      src={src}\n      alt={alt}\n      {...imageProps}\n    />\n  );\n};\n\nexport default Figure;\n"
  },
  {
    "path": "app/(without-root-layout)/p/components/img-container.tsx",
    "content": "const ImgContainer = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <div className=\"[&_img]:!object-contain [&_img]:!w-[300px]\">\n      {children}\n    </div>\n  );\n};\n\nexport default ImgContainer;\n"
  },
  {
    "path": "app/(without-root-layout)/p/components/small-text.tsx",
    "content": "const SmallText = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <div className=\"text-gray-11 text-[12px] flex flex-col\">{children}</div>\n  );\n};\n\nexport default SmallText;\n"
  },
  {
    "path": "app/(without-root-layout)/p/layout.tsx",
    "content": "import type { Viewport } from \"next\";\nimport Link from \"next/link\";\nimport styles from \"./substack-embed.module.css\";\n\nexport const viewport: Viewport = {\n  themeColor: \"#F9F2E2\",\n};\n\nconst Layout = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <div className=\"min-h-screen flex flex-col justify-between pt-0 md:pt-8 p-8 bg-[#F9F2E2] text-gray-12 text-[16px]\">\n      <nav className=\"max-w-[60ch] mx-auto w-full mt-6 flex gap-x-2.5 justify-between\">\n        <div className=\"flex gap-x-2.5 items-center\">\n          <Link\n            href=\"/\"\n            className=\"underline underline-offset-4 decoration-from-font\"\n          >\n            Home\n          </Link>\n          <Link\n            href=\"/visitors\"\n            className=\"underline underline-offset-4 decoration-from-font\"\n          >\n            Guestbook\n          </Link>\n        </div>\n        <Link\n          href=\"https://x.com/typicalmitul\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"underline underline-offset-4 decoration-from-font\"\n        >\n          @typicalmitul\n        </Link>\n      </nav>\n      <main className=\"max-w-[60ch] mx-auto w-full space-y-6 pb-12\">\n        {children}\n      </main>\n      <div className={styles.wrapper}>\n        <iframe\n          src=\"https://fieldnotesbymitul.substack.com/embed\"\n          className={styles.iframe}\n          frameBorder=\"0\"\n          scrolling=\"no\"\n        />\n        <div className={styles.overlay} />\n        <div className={styles.logoCover} />\n      </div>\n      <footer className=\"max-w-[60ch] mx-auto w-full text-left\">\n        <p className=\"text-[10px] text-gray-10 font-medium\">\n          from <span className=\"line-through\">toronto</span> nyc, with love /\n          typicalmitul\n        </p>\n      </footer>\n    </div>\n  );\n};\n\nexport default Layout;\n"
  },
  {
    "path": "app/(without-root-layout)/p/q1-2025/page.mdx",
    "content": "import Images from \"./two-imgs\";\nimport Figure from \"../components/figure\";\nimport SmallText from \"../components/small-text\";\nimport steps from \"./steps.jpg\";\nimport yt from \"./yt.png\";\nimport { BlogPostJsonLd } from \"@/components/json-ld\";\n\nexport const metadata = {\n  title: \"[Quarter Review] Q1/25 / Mitul Shah\",\n  description: \"Thriving on chaos\",\n  authors: [{ name: \"Mitul Shah\", url: \"https://mitul.ca\" }],\n  alternates: {\n    canonical: \"/p/q1-2025\",\n  },\n  openGraph: {\n    type: \"article\",\n    title: \"[Quarter Review] Q1/25 / Mitul Shah\",\n    description: \"Thriving on chaos\",\n    publishedTime: \"2025-03-31\",\n    authors: [\"Mitul Shah\"],\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"[Quarter Review] Q1/25 / Mitul Shah\",\n    description: \"Thriving on chaos\",\n  },\n};\n\n<BlogPostJsonLd slug=\"q1-2025\" />\n\n# Q1/25\n\n<Figure\n  src={steps}\n  alt=\"Les Marches du Palais Versailles, Henri Le Sidaner – 1925\"\n  caption=\"Les Marches du Palais Versailles, Henri Le Sidaner – 1925\"\n/>\n\nWell, it’s been an extremely eventful three months. With intentions of building stability [see [annual review](https://mitul.ca/p/2024)], it turned out to be everything but that. Things are good though, I’m thriving in a different way. I’ve been productive but not in the traditional sense where I’m working on my projects all day. Cliche, but I think it would be better said that I’m working on myself.\n\nOne of my primary goals for the year is to document more of my life. It has proved to be a better way to understand myself in a sense of where I’m good, or where I can make improvements, but also having a collection of memories I can reflect on. While spending a month in Argentina, I recorded a daily journal which was a [super rewarding experience](https://www.youtube.com/watch?v=J05WVHp5cJw&ab_channel=mitul).\n\n<Figure src={yt} />\n\nMy time in Argentina was more special than many of my other travels. I keep thinking of a word to describe it, and I always land on \"complete.\"\n\nEverything felt right. ¹\n\nI felt like I had everything I wanted, with a strong, fulfilling routine in my days that offered balance and stability. The trip was a reset moment for me.\n\nBefore I left for the trip, I let a stream of consciousness flow regarding my goals for the year, and without sounding dramatic, one of them was to expect radical honesty from everyone around me. I think what made Argentina so meaningful is that I actually found it — and in turn, started offering it too ².\n\nI don't expect to be travelling much this year, this is the first time in three years I'm not kicking off a long-term backpacking trip in April. It feels odd, but I remind myself my priorities and goals have changed. I have some trips but they're short or work related.\n\nHere's some highlights over the past few months, and things I continue to keep working towards:\n\n- Consistent with the gym, I hit 16 workouts in March!!\n- Fridays at [New Stadium](https://x.com/newsystems_/status/1887512072172241371) are so much fun\n- Working on cooking every meal of day\n- Spending time with people that value me as much as I them\n- Created a small [travel log](https://x.com/typicalmitul/status/1894073979733766157) of my time in Buenos Aires\n- Reading for at least ~20 minutes a day\n- Vercel had a company wide offsite in Monterrey, where I got to meet all my coworkers\n- I [did a podcast](https://open.spotify.com/episode/3ZFlTNlsLa8e2nYINUG0UF?si=c207029fd02a4087) to support my friend Rudy, I didn't think much about it initially until friends and strangers told me they enjoyed the listen\n\n### Going forward\n\nBy default, I’d consider myself a fairly open person, everything I do is shaped by my curiosity of the world. While acknowledging that, I decided to make a change this year and apply the same principles to things I have previously had an aversion to. It’s been teaching me a lot, while sometimes validating for why I was initially averse to certain things.\n\nRelationships have been changing. I've created distance in relationships where I felt I wasn't being offered the same level of transparency. And working on strengthening the relationships that matter to me, whether it's through hosting weekly dinners or simply consistently showing up.\n\nAt the moment, I am having trouble finding balance. I try to live my life with a max level of serendipity and spontaneity, but in reality, it's more closer to chaos. I want to slow things down and hopefully the intention of building stability will soon bring that.\n\n<Images />\n\nThings are still expected to significantly change soon, and hopefully the next update will be around that :)\n\nAnyways, thanks for reading, see you soon.\n\n---\n\n<SmallText>\n  1. man, it was probably just being in sun everyday in the middle of December\n  lol\n  <></>\n  2. I love an em-dash, leave me alone\n</SmallText>\n"
  },
  {
    "path": "app/(without-root-layout)/p/q1-2025/two-imgs.tsx",
    "content": "import img from \"./img.jpg\";\nimport img2 from \"./img2.jpg\";\nimport Figure from \"../components/figure\";\n\nconst Images = () => {\n  return (\n    <div className=\"relative grid gap-x-2 grid-cols-2 h-full items-center\">\n      <Figure src={img} alt=\"\" />\n      <Figure src={img2} alt=\"\" />\n    </div>\n  );\n};\n\nexport default Images;\n"
  },
  {
    "path": "app/(without-root-layout)/p/q2-2025/page.mdx",
    "content": "import { BlogPostJsonLd } from \"@/components/json-ld\";\n\nexport const metadata = {\n  title: \"[Quarter Review] Q2/25 / Mitul Shah\",\n  description: \"Moving to NYC\",\n  authors: [{ name: \"Mitul Shah\", url: \"https://mitul.ca\" }],\n  alternates: {\n    canonical: \"/p/q2-2025\",\n  },\n  openGraph: {\n    type: \"article\",\n    title: \"[Quarter Review] Q2/25 / Mitul Shah\",\n    description: \"Moving to NYC\",\n    publishedTime: \"2025-06-30\",\n    authors: [\"Mitul Shah\"],\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"[Quarter Review] Q2/25 / Mitul Shah\",\n    description: \"Moving to NYC\",\n  },\n};\n\n<BlogPostJsonLd slug=\"q2-2025\" />\n\n# Q2/25\n\nI finally moved to New York City. That's the most important part so might as well say it first. It's been everything I expected and nothing I could have prepared for, at the same time.\n\nEverything I wrote about in the [last review](/p/q1-2025); my gym routine, daily cooking and following through with documenting my life essentially disappeared. While acknowledging my time in Toronto was soon coming to an end, I consciously made the decision to let go of structure in service of spending time with my friends and family.\n\nThat kind of summarizes the last three months of my life in the best way possible. It all felt right, even with the ups and downs that came with it.\n\n![DSC02050.JPG](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/bf0f0160-8ef8-47ea-aa85-7a57632e95aa.jpeg)\n\n### Highlights\n\n- Hosted weekly dinners for my closest friends\n- Daytrip down to Buffalo to get the TN Visa approved\n- Trip to London for Figma Config and [Italy](/os/notes/italy) for two weeks (Naples, Palermo, back to Naples, Sorrento, Rome)\n- Reunited with so many friends in London. I feel extremely privileged and grateful to have friends in so many places of the world\n- Goodbye Party\n  - I brought together friends from all parts of my life\n  - My friend Daniil cooked for about ~35 people and I'm forever grateful\n- All my friends always give me cards / gifts that are so Toronto skyline related which has been cute\n- Moved to NYC\n  - My first few days were so deeply overwhelming, Toronto is almost, if not, everything I've ever known\n  - That feeling eventually faded, if there's anything I know about myself is that I'm capable of adapting quickly\n  - I'm loving my time, slowly and intentionally building up a new life\n- Vercel Ship\n\nI always thought I missed travelling, but I miss the memories I have of it. The little trip I had to Italy kind of validated that I'm no longer in dire need for it. Maybe I'll change my mind, but for now I'm so glad to be staying still.\n\n### What's next?\n\n- Finding a place to live and neighbourhood in NYC\n- Build a foundation, and introduce familiar routine back into my life\n- Stay connected to my friends back home\n- Let things unfold without forcing it\n\nIf you're in NYC, I'd love to hang out sometime. Being new means new friends, new outlets. I'm going to be dedicating more time to photography and video, so I'm keen to meet people who I can learn from, or potentially work with.\n\n![ .png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/concert-grid)\n\nThanks for reading, see you in a bit!\n"
  },
  {
    "path": "app/(without-root-layout)/p/q3-2025/page.mdx",
    "content": "import ImageContainer from \"../components/img-container\";\nimport Figure from \"../components/figure\";\nimport sunlight from \"./opengraph-image.jpg\";\nimport SmallText from \"../components/small-text\";\nimport { BlogPostJsonLd } from \"@/components/json-ld\";\n\nexport const metadata = {\n  title: \"[Quarter Review] Q3/25 / Mitul Shah\",\n  description: \"Three months in NYC: Starting from scratch\",\n  authors: [{ name: \"Mitul Shah\", url: \"https://mitul.ca\" }],\n  alternates: {\n    canonical: \"/p/q3-2025\",\n  },\n  openGraph: {\n    type: \"article\",\n    title: \"[Quarter Review] Q3/25 / Mitul Shah\",\n    description: \"Three months in NYC: Starting from scratch\",\n    publishedTime: \"2025-09-30\",\n    authors: [\"Mitul Shah\"],\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"[Quarter Review] Q3/25 / Mitul Shah\",\n    description: \"Three months in NYC: Starting from scratch\",\n  },\n};\n\n<BlogPostJsonLd slug=\"q3-2025\" />\n\n# Q3/25\n\n<Figure\n  src={sunlight}\n  alt=\"Boulevard des Italiens, Morning, Sunlight, Camille Pissarro – 1897\"\n  caption=\"Boulevard des Italiens, Morning, Sunlight, Camille Pissarro – 1897\"\n/>\n\nIt's hard to compress the past three months in a short write up, there's just so much that has happened. There's a lot about moving to a new country that I didn't account for, that I didn't expect both positive and bad.\n\nThe first few weeks or so were filled with me navigating the bureaucratic systems and being a degenerate. I was soaking in as much of the New York City summer as I possibly could, while being confused on how to get a social security number, with frequent reminders that they don't call it a SIN here.\n\nI spent 6 weeks subletting in Bushwick, with retired Bengali parents as my roommates which I was happy about as it alleviated some stresses while I figured out the city¹. Being in Bushwick as a landing spot was an experience in itself, with visiting grocery shops and speaking only Spanish. This later inspired me to double down on learning more Spanish and sign up for 8 weeks of classes.\n\nI moved onto Williamsburg for a month, at the Bedford stop, and hated it to say the least.\n\nI ended up settling in the Lower East Side because ChatGPT (and friends) told me it's a neighbourhood the most similar to ones I enjoyed from home – it wasn't wrong. With it being my first year here, it just felt like it made sense to put myself in the heart of it all.\n\nI got my first ever apartment, the bonus is it being in New York City.\n\n<ImageContainer>\n  ![\n  .png](https://inqeleafibjx2dzc.public.blob.vercel-storage.com/IMG_8456.jpeg)\n</ImageContainer>\n\nThings in New York move extremely fast. I have made countless friends, I have met countless people that I can't remember. My phone filled with unsaved numbers, my Instagram filled with stories of people I met for a few minutes and will never see again. In the span of three months, I have also managed to lose friends. It's a little insane, but there's a part of me that absolutely loves the rush.\n\nDay to day life has differed a bit as time has passed. Week one, I find myself in a Ridgewood grocery store, a bit excited that I can practice my Spanish and such, but by week four I'm just trying to find paneer between all the Oaxacan cheese. You notice there's tiny things you didn't think about – ranging between realizing you actually won't find your favourite brands to being confused because the bread tag doesn't have an expiry date.\n\nThings are different in the most subtle ways; politics are more integral, holidays are more widely celebrated, and nobody cares if you bump into them.\n\nI’ve essentially lived out of a backpack for the past three years, jumping between countries, sublets, random hostels and whatever. I don't own much stuff nor have I ever felt that I need much stuff but obviously I can't live in a house without any furniture. It's been a bit of a challenge forcing myself to accumulate things; stuff that does make me happy and adds value to my life but it's a different… playing field I guess.\n\nBut now that I do have an apartment that I am slowly furnishing to my taste and at a pace I'm comfortable with. I can find my grounding. I'm building out friend groups, routines, and I guess more wholly said, building a new life.\n\nI don't want to make this entire reflection about New York (I did) but I'll leave it with a final point: there's a certain level of openness in this city that I have never been able to find anywhere else, and it feels so fitting for me. I love being able to interact with anyone, and the fact that anyone feels comfortable enough to interact with me.\n\nToronto is home, and I do miss it in the sense of familiarity and my friends. It felt odd going from a goodbye party with my closest friends, to celebrating my birthday with people I have yet to know. Yes, there’s something novel about it but sometimes, you just wish you had your favourite bar around the corner instead of finding the replacement.\n\n--\n\nI'm grateful to live a life where I am able to chase my dreams. I looked through the goals I set for myself earlier this year, and I had achieved nearly everything I wrote – even the ones I thought were far fetched. Maybe I should be aiming higher.\n\n### What's next?\n\nHonestly, just settling in and building out a life. Still furnishing my apartment, hosting more events and so on. I've started a new photography project that I'm constantly thinking about. I think I have to take some time and think about what my next long term goals are.\n\nI finished writing this when there's one month left in the year, so I'll start working on that annual review right now…\n\n<SmallText>\n  1. They treated me like one of their own! My first time having roommates and\n  they were incredibly sweet\n</SmallText>\n"
  },
  {
    "path": "app/(without-root-layout)/p/substack-embed.module.css",
    "content": ".wrapper {\n  position: relative;\n  width: 480px;\n  max-width: 100%;\n  height: 150px;\n  margin: -20px auto 2rem;\n}\n\n.iframe {\n  width: 100%;\n  height: 100%;\n  border: none;\n}\n\n/* .overlay {\n  position: absolute;\n  inset: 0;\n  background: #f9f2e2;\n  mix-blend-mode: multiply;\n  pointer-events: none;\n} */\n\n.logoCover {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  width: 100px;\n  height: 28px;\n  background: #f9f2e2;\n  pointer-events: none;\n}\n"
  },
  {
    "path": "app/(without-root-layout)/visitors/actions.ts",
    "content": "\"use server\";\n\nimport { z } from \"zod\";\nimport { saveGuestbookEntry } from \"@/app/actions\";\nimport { sql } from \"@vercel/postgres\";\n\nconst GuestbookEntrySchema = z.object({\n  created_by: z\n    .string()\n    .min(1, \"pls fill out all fields\")\n    .max(50, \"ur name is too long\"),\n  entry: z\n    .string()\n    .min(1, \"pls fill out all fields\")\n    .max(200, \"love ur long entry, but can u make it shorter?\"),\n\n  signature: z.string().optional(),\n  local_entry_id: z.string().optional(),\n  hasCreatedEntryBefore: z.string().optional(),\n  local_created_by_id: z.string().optional(),\n});\n\nexport async function validateAndSaveEntry(\n  formData: FormData,\n  validateOnly = false\n) {\n  try {\n    GuestbookEntrySchema.parse(Object.fromEntries(formData));\n\n    if (validateOnly) {\n      return { success: true };\n    }\n\n    await saveGuestbookEntry(\"\", formData);\n\n    sendEmail(formData);\n\n    return { success: true };\n  } catch (error) {\n    if (error instanceof z.ZodError) {\n      return { success: false, errors: error.flatten().fieldErrors };\n    }\n    return {\n      success: false,\n      errors: { form: [\"An unexpected error occurred\"] },\n    };\n  }\n}\n\nasync function sendEmail(formData: FormData) {\n  try {\n    const response = await fetch(\"https://mitul.ca/api/send\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({ entry: Object.fromEntries(formData) }),\n      keepalive: true,\n    });\n\n    if (!response.ok) {\n      console.error(\"Failed to send email:\", await response.text());\n    }\n  } catch (error) {\n    console.error(\"Error sending email:\", error);\n  }\n}\n\nexport const getGuestbookEntries = async () => {\n  const { rows } = await sql`\n      SELECT * FROM \"guestbook\"\n  WHERE approved = true AND id > (\n    SELECT FLOOR(RANDOM() * (SELECT MAX(id) FROM guestbook WHERE approved = true))\n  )\n  ORDER BY id\n  LIMIT 30;\n  `;\n\n  return rows;\n};\n"
  },
  {
    "path": "app/(without-root-layout)/visitors/all/page.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport Note from \"@/components/visitors/note\";\nimport { cn } from \"@/lib/utils\";\nimport { sql } from \"@vercel/postgres\";\nimport styles from \"./visitors-all.module.css\";\nimport LinkPrimitive from \"@/components/link-primitive\";\nimport { Suspense } from \"react\";\nimport Link from \"next/link\";\n\nconst ITEMS_PER_PAGE = 50;\n\nexport default function Page(\n  props: {\n    searchParams: Promise<{ page?: string }>;\n  }\n) {\n  return (\n    <div className=\"relative\">\n      <div className=\"fixed top-8 left-8 text-gray-2 z-10 isolate flex gap-x-4\">\n        <LinkPrimitive\n          href=\"/visitors\"\n          variant=\"route\"\n          className=\"rounded-full text-gray-12 px-3 shadow-md font-medium\"\n        >\n          return to the visitor's log\n        </LinkPrimitive>\n        <LinkPrimitive\n          href=\"/\"\n          variant=\"route\"\n          className=\"rounded-full text-gray-12 px-3 shadow-md font-medium\"\n        >\n          return home\n        </LinkPrimitive>\n      </div>\n      <Suspense fallback={<p className=\"text-center mt-12\">Loading entries...</p>}>\n        <PageContent searchParams={props.searchParams} />\n      </Suspense>\n    </div>\n  );\n}\n\nasync function PageContent({ searchParams }: { searchParams: Promise<{ page?: string }> }) {\n  const params = await searchParams;\n  const currentPage = Number(params.page) || 1;\n\n  return (\n    <Suspense key={currentPage} fallback={<p>Loading entries...</p>}>\n      <EntriesList currentPage={currentPage} />\n    </Suspense>\n  );\n}\n\nasync function EntriesList({ currentPage }: { currentPage: number }) {\n  const { entries, totalPages } = await getGuestbookEntries(currentPage);\n\n  if (!entries.length) {\n    return <p>No entries found.</p>;\n  }\n\n  return (\n    <div className=\"flex flex-col items-center\">\n      <div\n        className={cn(\n          \"flex flex-wrap gap-x-4 gap-y-4 *:relative! *:transform-none! *:rotate-0! py-12\",\n          styles.container\n        )}\n      >\n        {entries.map((entry) => (\n          <Note\n            key={entry.id}\n            name={entry.created_by}\n            content={entry.body}\n            signature={entry.signature}\n          />\n        ))}\n      </div>\n      <Pagination currentPage={currentPage} totalPages={totalPages} />\n    </div>\n  );\n}\n\nfunction Pagination({\n  currentPage,\n  totalPages,\n}: {\n  currentPage: number;\n  totalPages: number;\n}) {\n  return (\n    <div className=\"flex justify-center space-x-2 mt-8 text-[black] pb-12\">\n      <Link\n        href={`/visitors/all?page=${currentPage - 1}`}\n        aria-disabled={currentPage <= 1}\n        className={cn(currentPage <= 1 ? \"pointer-events-none\" : \"\")}\n        tabIndex={currentPage <= 1 ? -1 : undefined}\n      >\n        <Button variant=\"outline\" disabled={currentPage <= 1}>\n          Previous\n        </Button>\n      </Link>\n      <span className=\"py-2 px-3 bg-gray-100 rounded\">\n        Page {currentPage} of {totalPages}\n      </span>\n      <Link\n        href={`/visitors/all?page=${currentPage + 1}`}\n        aria-disabled={currentPage >= totalPages}\n        className={cn(currentPage >= totalPages ? \"pointer-events-none\" : \"\")}\n        tabIndex={currentPage >= totalPages ? -1 : undefined}\n      >\n        <Button variant=\"outline\" disabled={currentPage >= totalPages}>\n          Next\n        </Button>\n      </Link>\n    </div>\n  );\n}\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\n\nButton.displayName = \"Button\";\n\nasync function getGuestbookEntries(page: number) {\n  const offset = (page - 1) * ITEMS_PER_PAGE;\n  try {\n    const [entriesResult, countResult] = await Promise.all([\n      sql`\n        SELECT id, created_by, body, signature\n        FROM guestbook\n        WHERE approved = true\n        ORDER BY last_modified DESC\n        LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}\n      `,\n      sql`SELECT COUNT(*) FROM guestbook WHERE approved = true`,\n    ]);\n\n    const totalEntries = Number(countResult.rows[0].count);\n    const totalPages = Math.ceil(totalEntries / ITEMS_PER_PAGE);\n\n    return {\n      entries: entriesResult.rows,\n      totalPages,\n    };\n  } catch (error) {\n    console.error(\"Failed to fetch guestbook entries:\", error);\n    throw new Error(\"Failed to fetch guestbook entries\");\n  }\n}\n"
  },
  {
    "path": "app/(without-root-layout)/visitors/all/visitors-all.module.css",
    "content": ".container {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.container :global(.note-item) {\n  box-shadow: none;\n  transform: none !important;\n  max-width: 100% !important;\n}\n"
  },
  {
    "path": "app/(without-root-layout)/visitors/gang/page.tsx",
    "content": "import ApproveButton from \"@/components/visitors/approve-btn\";\n\nimport { sql } from \"@vercel/postgres\";\nimport { cookies } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { Suspense } from \"react\";\n\nexport default function ProtectedPage() {\n  return (\n    <Suspense fallback={<div className=\"bg-gray-1 text-gray-12 p-12 h-screen\">Loading...</div>}>\n      <AuthenticatedContent />\n    </Suspense>\n  );\n}\n\nasync function AuthenticatedContent() {\n  const cookieStore = await cookies();\n  const isAuthenticated = cookieStore.get(\"auth\");\n\n  if (!isAuthenticated) {\n    redirect(\"/visitors/login\");\n  }\n\n  return (\n    <div className=\"bg-gray-1 text-gray-12 p-12 h-screen\">\n      <h1>Welcome to the gang page</h1>\n      <p>Only authenticated users can access this page.</p>\n      <Suspense fallback={<div className=\"text-gray-11\">Loading entries...</div>}>\n        <div\n          className=\"grid gap-4 mt-6\"\n          style={{\n            gridTemplateColumns: \"repeat(auto-fill, minmax(300px, 1fr))\",\n          }}\n        >\n          <GuestbookEntries />\n        </div>\n      </Suspense>\n    </div>\n  );\n}\n\nconst colorMap = new Map();\nconst colors = [\"red\", \"green\", \"blue\", \"orange\", \"purple\", \"yellow\"];\n\n// we gotta improve this a bit to be easier to scan\nfunction getColor(id: string) {\n  if (!colorMap.has(id)) {\n    const color = colors[colorMap.size % colors.length];\n    colorMap.set(id, color);\n  }\n  return colorMap.get(id);\n}\n\nasync function GuestbookEntries() {\n  const { rows } =\n    await sql`SELECT * from \"guestbook\" WHERE approved = false ORDER BY id DESC;`;\n\n  return rows.map((entry) => (\n    <div key={entry.id} className=\"w-fit h-fit border border-gray-10 p-2\">\n      <div className=\"border border-gray-6 bg-gray-3 rounded-[3px] flex items-center justify-center overflow-hidden\">\n        <div\n          className=\"object-contain\"\n          // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n          dangerouslySetInnerHTML={{ __html: entry.signature }}\n        />\n      </div>\n      <div className=\"w-full text-sm break-words mt-1.5\">\n        <span className=\"text-gray-12 text-[14px] mr-1 font-semibold\">\n          {entry.created_by}\n        </span>\n        <div className=\"text-[16px] font-medium\">{entry.body}</div>\n      </div>\n      <div className=\"flex flex-col\">\n        <span>\n          {entry.hascreatedentrybefore ? \"has created an entry before\" : \"new\"}\n        </span>\n\n        {entry.local_created_by_id && (\n          <span style={{ color: getColor(entry.local_created_by_id) }}>\n            created by: {entry.local_created_by_id}\n          </span>\n        )}\n      </div>\n      <ApproveButton id={entry.id} />\n    </div>\n  ));\n}\n"
  },
  {
    "path": "app/(without-root-layout)/visitors/login/page.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\nexport default function LoginPage() {\n  const [password, setPassword] = useState(\"\");\n  const router = useRouter();\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    const response = await fetch(\"/api/login\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ password }),\n    });\n\n    if (response.ok) {\n      router.push(\"/visitors/gang\");\n    } else {\n      alert(\"Incorrect password\");\n    }\n  };\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <input\n        type=\"password\"\n        value={password}\n        onChange={(e) => setPassword(e.target.value)}\n        placeholder=\"Enter password\"\n      />\n      <button type=\"submit\">Login</button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "app/(without-root-layout)/visitors/notes.module.css",
    "content": ".matContainer {\n  border-radius: 10px;\n  backdrop-filter: blur(10px);\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.2),\n    0 16px 32px rgba(0, 0, 0, 0.2);\n  background-color: #0a4a31;\n}\n\n.matContainer:after {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  border-radius: 10px;\n  box-shadow: inset 0 0 0 1.5px #fff6;\n}\n\n.matGrid {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-image: linear-gradient(\n      to right,\n      rgba(255, 255, 255, 0.1) 1px,\n      transparent 1px\n    ),\n    linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px);\n  background-size: 2vmin 2vmin;\n  background-position: center;\n  border-radius: 6px;\n  margin: 30px;\n  border: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.matGrid::after {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-image: linear-gradient(\n      to right,\n      rgba(255, 255, 255, 0.2) 1px,\n      transparent 1px\n    ),\n    linear-gradient(to bottom, rgba(255, 255, 255, 0.2) 1px, transparent 1px);\n  background-size: 10vmin 10vmin;\n  background-position: center;\n}\n\n@media (max-width: 768px) {\n  .matGrid {\n    background-size: 10vmin 10vmin;\n    margin: 10px;\n  }\n\n  .matGrid:after {\n    background-size: 30vmin 30vmin;\n  }\n}\n\n.matTexture {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n\n  background: linear-gradient(\n      45deg,\n      rgba(255, 255, 255, 0.2) 0%,\n      rgba(255, 255, 255, 0) 60%\n    ),\n    url(/noise.svg);\n  opacity: 0.75;\n  mix-blend-mode: overlay;\n  pointer-events: none;\n}\n\n.window {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-image: url(/images/Layer-88.png);\n  background-size: cover;\n  background-position: center;\n  opacity: 0.3;\n  pointer-events: none;\n  mix-blend-mode: normal;\n  z-index: 10000;\n  animation: window-shadow 12s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite\n    alternate;\n  border-radius: 10px;\n  user-select: none;\n}\n\n@keyframes window-shadow {\n  0% {\n    transform: translate(0px, 0px);\n    opacity: 0.5;\n    scale: 1.05;\n  }\n  50% {\n    transform: translate(20px, 10px);\n    opacity: 0.3;\n    scale: 1.05;\n  }\n  100% {\n    transform: translate(30px, 5px);\n    opacity: 0.2;\n    scale: 1.05;\n  }\n}\n\n.diagonalLines {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-image: linear-gradient(\n      45deg,\n      transparent 49.5%,\n      rgba(255, 255, 255, 0.2) 49.5%,\n      rgba(255, 255, 255, 0.2) 50.5%,\n      transparent 50.5%\n    ),\n    linear-gradient(\n      -45deg,\n      transparent 49.5%,\n      rgba(255, 255, 255, 0.2) 49.5%,\n      rgba(255, 255, 255, 0.2) 50.5%,\n      transparent 50.5%\n    );\n  background-size: 10vmin 10vmin;\n  background-position: center;\n  pointer-events: none;\n}\n\n.clip {\n  position: absolute;\n  top: 0px;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 60px;\n  transform: rotate(-8deg);\n}\n\n.homeBtn {\n  box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px,\n    rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset,\n    inset 0 1px 0 0 #ffffff52;\n}\n\n.stamp {\n  position: absolute;\n  top: 5vmin;\n  right: 5vmin;\n  width: 11vmin;\n  height: 11vmin;\n  border: 2px solid rgba(255, 255, 255, 0.5);\n  border-radius: 50%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  color: rgba(255, 255, 255, 0.5);\n  word-wrap: break-word;\n  word-spacing: 1vmin;\n  text-transform: uppercase;\n  text-align: center;\n  transform: rotate(15deg);\n  font-weight: 600;\n  font-size: 1.25vmin;\n  letter-spacing: -0.05px;\n  user-select: none;\n}\n\n.star {\n  position: absolute;\n  bottom: 3.5vmin;\n  left: 5vmin;\n  width: 11vmin;\n  height: 11vmin;\n  transform: rotate(-15deg);\n  mix-blend-mode: hard-light;\n  filter: invert(60%) blur(0.6px);\n}\n\n/* .moreNoise {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-image: url(/images/024.png);\n  opacity: 0.6;\n  pointer-events: none;\n  mix-blend-mode: screen;\n}\n*/\n"
  },
  {
    "path": "app/(without-root-layout)/visitors/page.tsx",
    "content": "import type { Viewport } from \"next\";\nimport { Provider } from \"jotai\";\nimport { cn } from \"@/lib/utils\";\nimport styles from \"./notes.module.css\";\nimport Polaroid from \"@/components/visitors/polaroid\";\nimport WriteNoteCTA from \"@/components/visitors/cta\";\nimport { ArrowLeft } from \"@phosphor-icons/react/dist/ssr/ArrowLeft\";\nimport { ArrowRight } from \"@phosphor-icons/react/dist/ssr/ArrowRight\";\nimport Link from \"next/link\";\nimport {\n  VercelLogo,\n  Sticker,\n  NextWordmark,\n} from \"@/components/visitors/stickers\";\nimport Image from \"next/image\";\nimport GuestbookEntries from \"@/components/visitors/guestbook-entries\";\nimport { Suspense } from \"react\";\n\n\nexport const viewport: Viewport = {\n  themeColor: \"#FCFCFC\",\n};\n\nconst Page = () => {\n  return (\n    <Provider>\n      <div className={cn(\"h-[100dvh] sm:h-[100vh] p-1 sm:p-6 bg-gray-1\")}>\n        <div\n          id=\"mat-container\"\n          className={cn(\n            \"relative w-full h-full overflow-hidden\",\n            styles.matContainer\n          )}\n        >\n          <div className=\"z-10\">\n            <div id=\"mat-texture\" className={styles.matTexture} />\n            <div aria-hidden className={styles.window} />\n            <div aria-hidden className={styles.star}>\n              <Image\n                alt=\"star drawing\"\n                width={80}\n                height={80}\n                src=\"/images/Star_002.png\"\n              />\n            </div>\n            {/* <div aria-hidden className={styles.moreNoise} /> */}\n            <div id=\"mat-grid\" className={styles.matGrid}>\n              <div id=\"diagonal-lines\" className={styles.diagonalLines} />\n            </div>\n          </div>\n          <main className=\"relative z-20 h-full w-full\">\n            <div className={styles.stamp}>\n              <span>Premium made in toronto quality</span>\n            </div>\n\n            <Suspense fallback={null}>\n              <GuestbookEntries />\n              <Polaroid src=\"/images/banff-2.jpg\" alt=\"toronto\" />\n              <Polaroid src=\"/images/toronto.jpg\" alt=\"toronto\" />\n              <Polaroid src=\"/images/nyc.jpg\" alt=\"toronto\" />\n              <Sticker>\n                <img\n                  className=\"w-36\"\n                  src=\"/images/spiderman.png\"\n                  alt=\"Spiderman sticker\"\n                  draggable={false}\n                />\n              </Sticker>\n              <Sticker>\n                <img\n                  className=\"w-24\"\n                  src=\"/images/cntower.png\"\n                  alt=\"CN Tower sticker\"\n                  draggable={false}\n                />\n              </Sticker>\n              <Sticker>\n                <VercelLogo />\n              </Sticker>\n              <Sticker>\n                <NextWordmark />\n              </Sticker>\n            </Suspense>\n\n            <Link\n              href=\"/\"\n              className={cn(\n                \"mr-auto rounded-full bg-[#027582] hover:bg-[#027582]/90 transition hover:scale-105 hover:-rotate-6 px-3 py-1.5 flex gap-x-1.5 items-center justify-center text-gray-1 font-semibold w-fit h-fit z-50 absolute top-4 left-4 sm:top-10 sm:left-10\",\n                styles.homeBtn\n              )}\n            >\n              <ArrowLeft width={16} height={16} />\n              take me home\n            </Link>\n            <Link\n              href=\"/visitors/all\"\n              className={cn(\n                \"mr-auto rounded-full bg-[#027582] hover:bg-[#027582]/90 transition hover:scale-105 hover:-rotate-6 px-3 py-1.5 flex gap-x-1.5 items-center justify-center text-gray-1 font-semibold w-fit h-fit z-50 absolute top-14 left-4 sm:bottom-10 sm:right-10 sm:left-auto sm:top-auto\",\n                styles.homeBtn\n              )}\n            >\n              see all notes\n              <ArrowRight width={16} height={16} />\n            </Link>\n            <WriteNoteCTA />\n          </main>\n        </div>\n      </div>\n    </Provider>\n  );\n};\n\nexport default Page;\n"
  },
  {
    "path": "app/actions.ts",
    "content": "\"use server\";\nimport { sql } from \"@vercel/postgres\";\nimport { revalidatePath } from \"next/cache\";\n\nexport async function saveGuestbookEntry(state: unknown, formData: FormData) {\n  const local_entry_id = formData.get(\"local_entry_id\")?.toString();\n  const created_by = formData.get(\"created_by\")?.toString() || \"\";\n  const signature = formData.get(\"signature\")?.toString() || \"\";\n  const hasCreatedEntryBefore = formData\n    .get(\"hasCreatedEntryBefore\")\n    ?.toString();\n  const local_created_by_id = formData.get(\"local_created_by_id\")?.toString();\n  const entry = formData.get(\"entry\")?.toString() || \"\";\n  const body = entry.slice(0, 500);\n\n  await sql`\n    INSERT INTO \"guestbook\" (created_by, body, last_modified, signature, hasCreatedEntryBefore, local_created_by_id, local_entry_id) \n    VALUES (${created_by}, ${body}, ${new Date().toISOString()}, ${signature}, ${hasCreatedEntryBefore}, ${local_created_by_id}, ${local_entry_id});\n  `;\n\n  revalidatePath(\"/visitors\");\n}\n\nexport async function approveGuestbookEntry(id: string) {\n  await sql`\n    UPDATE \"guestbook\" SET approved = true WHERE id = ${id};\n  `;\n\n  revalidatePath(\"/visitors\");\n}\n\nexport async function declineGuestbookEntry(id: string) {\n  await sql`\n    DELETE FROM \"guestbook\" WHERE id = ${id};\n  `;\n\n  revalidatePath(\"/visitors\");\n}\n"
  },
  {
    "path": "app/api/login/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { cookies } from \"next/headers\";\n\nexport async function POST(request: Request) {\n  const { password } = await request.json();\n\n  if (password === process.env.ROUTE_PASSWORD) {\n    (await cookies()).set(\"auth\", \"true\", { httpOnly: true });\n    return NextResponse.json({ success: true });\n  }\n  return NextResponse.json({ success: false }, { status: 401 });\n}\n"
  },
  {
    "path": "app/api/md/route.ts",
    "content": "import { NextRequest } from \"next/server\";\nimport { experiences, beliefs, bucketList, Status } from \"@/content\";\nimport { blogPosts, getBlogPost } from \"@/lib/blog-posts\";\nimport { readFileSync } from \"fs\";\nimport { join } from \"path\";\n\nconst BASE_URL = \"https://mitul.ca\";\n\nfunction getHomeMarkdown(): string {\n  return `# Mitul Shah\n\n> Design engineer, photographer, and a bit more.\n\nCrafting memorable interfaces with deep attention to detail. I dedicate most my time to continuous learning and refining my skillset.\n\n## Experience\n\n${experiences.map((exp) => `### ${exp.company} - ${exp.role} (${exp.range})\n${exp.description}\n${exp.link ? `[Visit](${exp.link})` : \"\"}`).join(\"\\n\\n\")}\n\n## Projects\n\n### Daybloom\nA mindful daily journal that combines your photos and thoughts into calendar view, helping you capture memories and reflect on life's meaningful moments.\n[Visit](https://daybloom.app)\n\n### Montreal in Motion\nA documentation of the brutalist and distinctly designed metro stations. The project uses CSS 3D transforms and noise to mirror the architectural character of the spaces.\n[Visit](https://typicalmitul.com/montreal-in-motion)\n\n### Places to Read\nA microsite to discover community submitted parks around the world where you can sit down, chill and enjoy reading a book.\n[Visit](https://placestoread.xyz)\n\n## Photography\n\nI've built up my craft as a photographer over a number of years and thrived in turning it into an independent business.\n\nNotable achievements include:\n- Being a personal photographer for the Uber CEO\n- Featured in local Toronto newspapers\n- Having a photo as a wallpaper in every Google device\n\nToday, my focus is on music photography where I capture my favourite artists at concerts or festivals.\n\n[Visit my portfolio](https://typicalmitul.com)\n\n## Contact\n\n- Email: mitulxshah@gmail.com\n- Twitter: [@typicalmitul](https://x.com/typicalmitul)\n- Instagram: [@typicalmitul](https://instagram.com/typicalmitul)\n- GitHub: [mitul-s](https://github.com/mitul-s)\n\n## Blog Posts\n\n${blogPosts.map((post) => `- [${post.title}](${BASE_URL}/p/${post.slug}) - ${post.description}`).join(\"\\n\")}\n`;\n}\n\nfunction getAboutMarkdown(): string {\n  return `# About Mitul Shah\n\nHey, my name is Mitul and welcome to my space on the internet. I'm a self-taught design engineer based in Toronto, Canada.\n\nLearning to code has felt like a superpower for me, it allows me to bring any idea I can imagine to life. I love creating and focusing on the little things that enhance our experiences as we dive into the abyss of the web.\n\nApart from all of that, a strong sense of curiosity about the world has always driven me. Travel, and specifically the diverse experiences gained from exploring different places, cultures, and landscapes, have significantly influenced my personal growth.\n\nMy life thrives on both chaos and serendipity. I'm just tryna channel the same spirit of adventure as Ferris Bueller.\n\n## Beliefs\n\n${beliefs.map((belief) => `- ${belief}`).join(\"\\n\")}\n\n## Bucket List\n\n${bucketList.map((item) => `- ${item.item}${item.status === Status.completed ? \" ✓\" : item.status === Status.progress ? \" (in progress)\" : \"\"}`).join(\"\\n\")}\n`;\n}\n\nfunction getBlogPostMarkdown(slug: string): string | null {\n  const post = getBlogPost(slug);\n  if (!post) return null;\n\n  // Try to read the MDX file content\n  try {\n    const mdxPath = join(\n      process.cwd(),\n      \"app/(without-root-layout)/p\",\n      slug,\n      \"page.mdx\"\n    );\n    let content = readFileSync(mdxPath, \"utf-8\");\n\n    // Remove import statements and export metadata\n    content = content\n      .replace(/^import.*$/gm, \"\")\n      .replace(/export const metadata = \\{[\\s\\S]*?\\};/m, \"\")\n      .replace(/<BlogPostJsonLd.*?\\/>/g, \"\")\n      .replace(/<Figure[\\s\\S]*?\\/>/g, (match) => {\n        // Extract alt text from Figure components\n        const altMatch = match.match(/alt=\"([^\"]*)\"/);\n        const srcMatch = match.match(/src=\\{?([^}\\s]*)\\}?/);\n        if (altMatch) return `*${altMatch[1]}*`;\n        return \"\";\n      })\n      .replace(/<ImageContainer>[\\s\\S]*?<\\/ImageContainer>/g, \"\")\n      .replace(/<Callout>[\\s\\S]*?<\\/Callout>/g, \"\")\n      .replace(/<SmallText>[\\s\\S]*?<\\/SmallText>/g, \"\")\n      .replace(/<[^>]+>/g, \"\") // Remove remaining JSX tags\n      .trim();\n\n    return `# ${post.title}\n\n*Published: ${post.datePublished}*\n\n${content}\n\n---\n[Back to home](${BASE_URL})\n`;\n  } catch {\n    // Fallback if we can't read the file\n    return `# ${post.title}\n\n${post.description}\n\n*Published: ${post.datePublished}*\n\n[Read the full article](${BASE_URL}/p/${slug})\n`;\n  }\n}\n\nfunction getVisitorsMarkdown(): string {\n  return `# Guestbook\n\nWelcome to the guestbook! This is an interactive page where visitors can leave notes.\n\n[Visit the guestbook](${BASE_URL}/visitors) to sign it.\n`;\n}\n\nexport async function GET(request: NextRequest) {\n  // Try searchParams first, then x-original-path header from proxy\n  const path =\n    request.nextUrl.searchParams.get(\"path\") ||\n    request.headers.get(\"x-original-path\") ||\n    \"/\";\n\n  let markdown: string;\n\n  if (path === \"/\" || path === \"\") {\n    markdown = getHomeMarkdown();\n  } else if (path === \"/about\") {\n    markdown = getAboutMarkdown();\n  } else if (path === \"/visitors\") {\n    markdown = getVisitorsMarkdown();\n  } else if (path.startsWith(\"/p/\")) {\n    const slug = path.replace(\"/p/\", \"\");\n    const content = getBlogPostMarkdown(slug);\n    if (content) {\n      markdown = content;\n    } else {\n      markdown = `# Not Found\\n\\nThe requested page was not found.`;\n    }\n  } else {\n    markdown = `# Not Found\\n\\nThe requested page was not found.`;\n  }\n\n  return new Response(markdown, {\n    headers: {\n      \"Content-Type\": \"text/markdown; charset=utf-8\",\n      \"Cache-Control\": \"public, max-age=3600, s-maxage=3600\",\n    },\n  });\n}\n"
  },
  {
    "path": "app/api/send/route.ts",
    "content": "import { EmailTemplate } from \"@/components/email-template\";\nimport { Resend } from \"resend\";\n\nconst resend = new Resend(process.env.RESEND_API_KEY);\n\nexport async function POST(request: Request) {\n  try {\n    const { entry } = await request.json();\n    const { data, error } = await resend.emails.send({\n      from: \"Guestbook <hi@daybloom.app>\",\n      to: [\"mitulxshah@gmail.com\"],\n      subject: \"New Submission!\",\n      react: await EmailTemplate({ entry }),\n    });\n\n    if (error) {\n      return Response.json({ error }, { status: 500 });\n    }\n\n    return Response.json(data);\n  } catch (error) {\n    return Response.json({ error }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "app/api/spotify/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { getSpotifyData } from \"@/lib/spotify\";\n\n\nexport async function GET() {\n  const data = await getSpotifyData();\n  return NextResponse.json(data);\n}\n"
  },
  {
    "path": "app/feed.xml/route.ts",
    "content": "import { blogPosts } from \"@/lib/blog-posts\";\n\nconst BASE_URL = \"https://mitul.ca\";\n\nfunction escapeXml(text: string): string {\n  return text\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .replace(/\"/g, \"&quot;\")\n    .replace(/'/g, \"&apos;\");\n}\n\nfunction generateRssFeed(): string {\n  const sortedPosts = [...blogPosts].sort(\n    (a, b) => new Date(b.datePublished).getTime() - new Date(a.datePublished).getTime()\n  );\n\n  const items = sortedPosts\n    .map((post) => {\n      const pubDate = new Date(post.datePublished).toUTCString();\n      return `\n    <item>\n      <title>${escapeXml(post.title)}</title>\n      <link>${BASE_URL}/p/${post.slug}</link>\n      <description>${escapeXml(post.description)}</description>\n      <pubDate>${pubDate}</pubDate>\n      <guid isPermaLink=\"true\">${BASE_URL}/p/${post.slug}</guid>\n    </item>`;\n    })\n    .join(\"\");\n\n  return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n  <channel>\n    <title>Mitul Shah</title>\n    <link>${BASE_URL}</link>\n    <description>Design engineer, photographer, and a bit more. Annual reviews and quarterly reflections.</description>\n    <language>en-us</language>\n    <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>\n    <atom:link href=\"${BASE_URL}/feed.xml\" rel=\"self\" type=\"application/rss+xml\"/>\n    ${items}\n  </channel>\n</rss>`;\n}\n\nexport function GET() {\n  const feed = generateRssFeed();\n\n  return new Response(feed, {\n    headers: {\n      \"Content-Type\": \"application/xml; charset=utf-8\",\n      \"Cache-Control\": \"public, max-age=3600, s-maxage=3600\",\n    },\n  });\n}\n"
  },
  {
    "path": "app/globals.css",
    "content": "@import \"@radix-ui/colors/gray.css\" layer(base);\n@import \"@radix-ui/colors/blue.css\" layer(base);\n\n@import \"tailwindcss\";\n\n[data-theme=\"green\"] {\n  --color-accent-rgb: 19, 50, 18;\n  --color-accent: rgb(var(--color-accent-rgb));\n  --color-accent-hex: #133212;\n}\n\n[data-theme=\"red\"] {\n  --color-accent-rgb: 178, 0, 36;\n  --color-accent: rgb(var(--color-accent-rgb));\n  --color-accent-hex: #b20024;\n}\n\n[data-theme=\"blue\"] {\n  --color-accent-rgb: 2, 16, 147;\n  --color-accent: rgb(var(--color-accent-rgb));\n  --color-accent-hex: #021093;\n}\n\n@theme {\n  --color-*: initial;\n  --color-gray-1: var(--gray-1);\n  --color-gray-2: var(--gray-2);\n  --color-gray-3: var(--gray-3);\n  --color-gray-4: var(--gray-4);\n  --color-gray-5: var(--gray-5);\n  --color-gray-6: var(--gray-6);\n  --color-gray-7: var(--gray-7);\n  --color-gray-8: var(--gray-8);\n  --color-gray-9: var(--gray-9);\n  --color-gray-10: var(--gray-10);\n  --color-gray-11: var(--gray-11);\n  --color-gray-12: var(--gray-12);\n  --color-gray-a1: var(--grayA-1);\n  --color-gray-a2: var(--grayA-2);\n  --color-gray-a3: var(--grayA-3);\n  --color-gray-a4: var(--grayA-4);\n  --color-gray-a5: var(--grayA-5);\n  --color-gray-a6: var(--grayA-6);\n  --color-gray-a7: var(--grayA-7);\n  --color-gray-a8: var(--grayA-8);\n  --color-gray-a9: var(--grayA-9);\n  --color-gray-a10: var(--grayA-10);\n  --color-gray-a11: var(--grayA-11);\n  --color-gray-a12: var(--grayA-12);\n\n  --color-blue-1: var(--blue-1);\n  --color-blue-2: var(--blue-2);\n  --color-blue-3: var(--blue-3);\n  --color-blue-4: var(--blue-4);\n  --color-blue-5: var(--blue-5);\n  --color-blue-6: var(--blue-6);\n  --color-blue-7: var(--blue-7);\n  --color-blue-8: var(--blue-8);\n  --color-blue-9: var(--blue-9);\n  --color-blue-10: var(--blue-10);\n  --color-blue-11: var(--blue-11);\n  --color-blue-12: var(--blue-12);\n  --color-blue-a1: var(--blueA-1);\n  --color-blue-a2: var(--blueA-2);\n  --color-blue-a3: var(--blueA-3);\n  --color-blue-a4: var(--blueA-4);\n  --color-blue-a5: var(--blueA-5);\n  --color-blue-a6: var(--blueA-6);\n  --color-blue-a7: var(--blueA-7);\n  --color-blue-a8: var(--blueA-8);\n  --color-blue-a9: var(--blueA-9);\n  --color-blue-a10: var(--blueA-10);\n  --color-blue-a11: var(--blueA-11);\n  --color-blue-a12: var(--blueA-12);\n\n  /* --color-accent: #b3fc03; */\n  /* --color-accent: #133212; */\n  /* --color-accent: #021093; */\n  --color-accent: #b20024;\n  /* --color-accent: #ff7f00; */\n  --color-light-green: #e2f3e3;\n  --color-white: #fff;\n\n  --text-*: initial;\n  --text-sm: 0.75rem;\n  --text-base: 0.875rem;\n\n  --radius-*: initial;\n  --radius-4: 4px;\n  --radius-6: 6px;\n  --radius-sm: 1px;\n  --radius-md: 2px;\n  --radius-full: 9999px;\n\n  --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops));\n  --background-image-gradient-conic: conic-gradient(\n    from 180deg at 50% 50%,\n    var(--tw-gradient-stops)\n  );\n\n  --animate-accordion-down: accordion-down 0.2s ease-out;\n  --animate-accordion-up: accordion-up 0.2s ease-out;\n  --animate-slide-up-and-fade: slide-up-and-fade 0.25s\n    cubic-bezier(0.16, 0, 0.13, 1);\n  --animate-slide-down-and-fade: slide-down-and-fade 0.25s\n    cubic-bezier(0.16, 0, 0.13, 1);\n\n  --animate-blur-and-slide-up: blur-and-slide-up 0.25s\n    cubic-bezier(0.16, 0, 0.13, 1);\n  --animate-blur-and-slide-down: blur-and-slide-down 0.25s\n    cubic-bezier(0.16, 0, 0.13, 1);\n\n  @keyframes accordion-down {\n    from {\n      height: 0;\n      opacity: 0;\n      filter: blur(4px);\n      transform: translateY(-1rem);\n    }\n    to {\n      height: var(--radix-accordion-content-height);\n      opacity: 1;\n      filter: blur(0);\n      transform: translateY(0);\n    }\n  }\n\n  @keyframes accordion-up {\n    from {\n      height: var(--radix-accordion-content-height);\n      opacity: 1;\n      filter: blur(0);\n    }\n    to {\n      height: 0;\n      opacity: 0;\n      filter: blur(4px);\n    }\n  }\n\n  @keyframes slide-up-and-fade {\n    0% {\n      opacity: 0;\n      transform: translateY(1rem);\n    }\n    100% {\n      opacity: 1;\n      transform: translateY(0);\n    }\n  }\n  @keyframes slide-down-and-fade {\n    0% {\n      opacity: 0;\n      transform: translateY(-1rem);\n    }\n    100% {\n      opacity: 1;\n      transform: translateY(0);\n    }\n  }\n\n  @keyframes blur-and-slide-up {\n    from {\n      opacity: 0;\n      filter: blur(4px);\n      transform: translateY(-1.5rem) scale(0.9);\n    }\n    to {\n      opacity: 1;\n      filter: blur(0);\n      transform: translateY(0) scale(1);\n    }\n  }\n\n  @keyframes blur-and-slide-down {\n    from {\n      opacity: 0;\n      filter: blur(4px);\n      transform: translateY(1.5rem) scale(0.9);\n    }\n    to {\n      opacity: 1;\n      filter: blur(0);\n      transform: translateY(0) scale(1);\n    }\n  }\n}\n\n/*\n  The default border color has changed to `currentColor` in Tailwind CSS v4,\n  so we've added these compatibility styles to make sure everything still\n  looks the same as it did with Tailwind CSS v3.\n\n  If we ever want to remove these styles, we need to add an explicit border\n  color utility to any element that depends on these defaults.\n*/\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentColor);\n  }\n}\n\n.select ::-moz-selection {\n  @apply text-accent bg-accent/10;\n}\n\n.select ::selection {\n  @apply text-accent bg-accent/10;\n}\n\na,\nbutton {\n  touch-action: manipulation;\n}\n\nhtml {\n  /* background-color: var(--color-accent); */\n}\n\nbody {\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  text-rendering: geometricPrecision;\n  font-family: \"Monument Grotesk\", sans-serif;\n  /* font-family: \"Helvetica Neue\", sans-serif; */\n  font-size: 14px;\n  /* background-color: white; */\n  /* background: linear-gradient(\n      to bottom,\n      transparent,\n      rgb(var(--background-end-rgb))\n    )\n    rgb(var(--background-start-rgb)); */\n}\n\n.bg {\n  background-image: linear-gradient(to right, #101010, rgba(0, 0, 0, 0.83)),\n    url(/noise.svg), linear-gradient(#b3fc03 1px, transparent 1px),\n    linear-gradient(to right, #b3fc03 1px, #000 1px);\n  background-size: auto, auto, 20px 20px, 20px 20px;\n}\n\n.accordion-grid-cols {\n  grid-template-columns: min-content 125px 1fr min-content min-content;\n}\n\n.main-noise {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  /* background: linear-gradient(\n      45deg,\n      rgba(255, 255, 255, 0.1) 0%,\n      var(--color-light-green) 100%\n    ),\n    url(/noise.svg); */\n  background: url(/noise.svg);\n  opacity: 0.4;\n  mix-blend-mode: overlay;\n  pointer-events: none;\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import { Analytics } from \"@vercel/analytics/react\";\nimport { SpeedInsights } from \"@vercel/speed-insights/next\";\nimport type { Metadata } from \"next\";\nimport localFont from \"next/font/local\";\nimport \"./globals.css\";\nimport { cn } from \"@/lib/utils\";\nimport { ThemeProvider } from \"next-themes\";\nimport { PersonJsonLd, WebSiteJsonLd } from \"@/components/json-ld\";\n\n// const monument = localFont({\n//   src: [\n//     {\n//       path: \"../public/font/ABCMonumentGrotesk-Medium-Trial.otf\",\n//       weight: \"500\",\n//       style: \"medium\",\n//     },\n//     {\n//       path: \"../public/font/ABCMonumentGrotesk-Regular-Trial.otf\",\n//       weight: \"400\",\n//       style: \"regular\",\n//     },\n//   ],\n// });\n\n// const helvetica = localFont({\n//   src: [\n//     {\n//       path: \"../public/font/HelveticaNeueBold.ttf\",\n//       weight: \"700\",\n//       style: \"bold\",\n//     },\n//     {\n//       path: \"../public/font/HelveticaNeue-Medium.otf\",\n//       weight: \"500\",\n//       style: \"medium\",\n//     },\n//     {\n//       path: \"../public/font/HelveticaNeue-Roman.otf\",\n//       weight: \"400\",\n//       style: \"regular\",\n//     },\n//   ],\n// });\n\nconst chico = localFont({\n  src: [\n    {\n      path: \"../public/font/PPNeueMontreal-SemiBold.woff2\",\n      weight: \"500\",\n      style: \"medium\",\n    },\n    {\n      path: \"../public/font/PPNeueMontreal-Medium.woff2\",\n      weight: \"400\",\n      style: \"regular\",\n    },\n  ],\n});\n\nexport const metadata: Metadata = {\n  title: {\n    template: \"%s / Mitul Shah\",\n    default: \"Mitul Shah / @typicalmitul\",\n  },\n  description: \"Design engineer, photographer, and a bit more.\",\n  openGraph: {\n    title: \"Mitul Shah\",\n    description: \"Design engineer, photographer, and a bit more.\",\n    images: \"/og-2.png\",\n    url: \"https://mitul.ca\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    site: \"@typicalmitul\",\n    creator: \"@typicalmitul\",\n  },\n  alternates: {\n    canonical: \"https://mitul.ca\",\n    types: {\n      \"application/rss+xml\": \"https://mitul.ca/feed.xml\",\n    },\n  },\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body className={cn(chico.className)}>\n        <PersonJsonLd />\n        <WebSiteJsonLd />\n        <ThemeProvider\n          themes={[\"blue\", \"green\", \"red\"]}\n          defaultTheme=\"blue\"\n          storageKey=\"ms-theme\"\n        >\n          {children}\n          <Analytics />\n          <SpeedInsights />\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/llms.txt/route.ts",
    "content": "import { experiences } from \"@/content\";\nimport { readdirSync } from \"fs\";\nimport { join } from \"path\";\n\nconst BASE_URL = \"https://mitul.ca\";\n\nfunction getBlogSlugs(): string[] {\n  const postsDir = join(process.cwd(), \"app/(without-root-layout)/p\");\n  try {\n    return readdirSync(postsDir, { withFileTypes: true })\n      .filter((entry) => entry.isDirectory())\n      .map((entry) => entry.name);\n  } catch {\n    return [];\n  }\n}\n\nfunction generateLlmsTxt(): string {\n  const blogSlugs = getBlogSlugs();\n\n  return `# Mitul Shah\n\n> Personal portfolio and blog of Mitul Shah, a design engineer and photographer based in New York City (originally from Toronto, Canada).\n\n## About\n\nMitul is a self-taught design engineer currently working at Vercel. He crafts memorable interfaces with deep attention to detail and dedicates time to continuous learning. He's also a professional photographer specializing in music photography, with notable achievements including being a personal photographer for the Uber CEO and having photos featured as wallpapers on Google devices.\n\n## Contact\n\n- Email: mitulxshah@gmail.com\n- Twitter/X: @typicalmitul\n- Instagram: @typicalmitul\n- GitHub: mitul-s\n\n## Pages\n\n- [Home](${BASE_URL}): Main portfolio with experience, projects, photography, and contact info\n- [About](${BASE_URL}/about): Personal background, beliefs, and bucket list\n- [Guestbook](${BASE_URL}/visitors): Interactive guestbook for visitors to leave notes\n- [Photography Portfolio](https://typicalmitul.com): External photography portfolio site\n\n## Blog Posts\n\n${blogSlugs.map((slug) => `- [${slug}](${BASE_URL}/p/${slug})`).join(\"\\n\")}\n\n## Experience\n\n${experiences.map((exp) => `- **${exp.company}** (${exp.range === \"Now\" ? \"Current\" : exp.range}): ${exp.role} - ${exp.description}`).join(\"\\n\")}\n\n## Projects\n\n- **Daybloom** (https://daybloom.app): A mindful daily journal combining photos and thoughts into calendar view\n- **Montreal in Motion** (https://typicalmitul.com/montreal-in-motion): CSS 3D documentation of Montreal's brutalist metro stations\n- **Places to Read** (https://placestoread.xyz): Community-submitted parks for reading around the world\n`;\n}\n\nexport function GET() {\n  return new Response(generateLlmsTxt(), {\n    headers: {\n      \"Content-Type\": \"text/plain; charset=utf-8\",\n      \"Cache-Control\": \"public, max-age=3600, s-maxage=3600\",\n    },\n  });\n}\n"
  },
  {
    "path": "app/robots.txt",
    "content": "User-Agent: *\nAllow: /\nDisallow: /api/\nDisallow: /visitors/login\n\nSitemap: https://mitul.ca/sitemap.xml\n"
  },
  {
    "path": "app/sitemap.ts",
    "content": "import type { MetadataRoute } from \"next\";\nimport { readdirSync } from \"fs\";\nimport { join } from \"path\";\n\nconst BASE_URL = \"https://mitul.ca\";\n\nfunction getBlogSlugs(): string[] {\n  const postsDir = join(process.cwd(), \"app/(without-root-layout)/p\");\n  try {\n    return readdirSync(postsDir, { withFileTypes: true })\n      .filter((entry) => entry.isDirectory())\n      .map((entry) => entry.name);\n  } catch {\n    return [];\n  }\n}\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n  const blogSlugs = getBlogSlugs();\n\n  return [\n    {\n      url: BASE_URL,\n      lastModified: new Date(),\n      changeFrequency: \"monthly\",\n      priority: 1,\n    },\n    {\n      url: `${BASE_URL}/about`,\n      lastModified: new Date(),\n      changeFrequency: \"monthly\",\n      priority: 0.8,\n    },\n    {\n      url: `${BASE_URL}/visitors`,\n      lastModified: new Date(),\n      changeFrequency: \"weekly\",\n      priority: 0.6,\n    },\n    ...blogSlugs.map((slug) => ({\n      url: `${BASE_URL}/p/${slug}`,\n      lastModified: new Date(),\n      changeFrequency: \"yearly\" as const,\n      priority: 0.7,\n    })),\n  ];\n}\n"
  },
  {
    "path": "atoms/guestbook.tsx",
    "content": "import { atom } from \"jotai\";\nimport { atomWithStorage } from \"jotai/utils\";\n\nexport type Entry = {\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  initialX?: any;\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  initialY?: any;\n  id: string;\n  created_by: string;\n  local_entry_id?: string;\n  body: string;\n  signature: string;\n  approved?: boolean;\n  last_modified?: string;\n};\n\nexport const serverEntriesAtom = atom<Entry[]>([]);\nexport const localEntriesAtom = atomWithStorage<Entry[]>(\n  \"localGuestbookEntries\",\n  []\n);\n\nexport const hasCreatedEntryBeforeAtom = atomWithStorage(\n  \"hasCreatedEntryBefore\",\n  false\n);\nexport const localCreatedByIdAtom = atomWithStorage(\"localCreatedById\", \"\");\n\nexport const allEntriesAtom = atom((get) => {\n  const serverEntries = get(serverEntriesAtom);\n  const localEntries = get(localEntriesAtom);\n  return [...localEntries, ...serverEntries];\n});\n"
  },
  {
    "path": "components/collapsible.tsx",
    "content": "\"use client\";\n\nimport { CaretRight } from \"@phosphor-icons/react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\n\n//@ts-ignore\nimport useSound from \"use-sound\";\nimport { ArrowUpRight } from \"@phosphor-icons/react/dist/ssr/ArrowUpRight\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useState } from \"react\";\n\ninterface ExpandingLinkProps {\n  link: string;\n  company: string;\n}\n\nexport const ExpandingLink: React.FC<ExpandingLinkProps> = ({\n  link,\n  company,\n}) => {\n  const [isHovered, setIsHovered] = useState(false);\n\n  return (\n    <motion.a\n      className=\"cursor-pointer text-sm z-[2] flex items-center overflow-clip hover:group-data-[state=open]:bg-accent hover:text-gray-1 transition-colors duration-100 ease-in opacity-0 group-data-[state=open]:opacity-100 group-data-[state=open]:text-accent group-hover:opacity-100 text-gray-1 h-4\"\n      animate={{ width: isHovered ? \"auto\" : \"20px\" }}\n      onHoverStart={() => setIsHovered(true)}\n      onHoverEnd={() => setIsHovered(false)}\n      transition={{\n        type: \"spring\",\n        stiffness: 500,\n        damping: 30,\n        mass: 0.5,\n      }}\n      href={link}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      aria-label={`Link to ${company} website`}\n      onClick={(e) => e.stopPropagation()}\n      layout\n    >\n      <ArrowUpRight\n        aria-hidden={true}\n        size={12}\n        className=\"shrink-0 ml-0.5 translate-y-[0.5px]\"\n      />\n\n      <AnimatePresence>\n        {isHovered && (\n          <motion.span\n            initial={{ opacity: 0, width: 0 }}\n            animate={{ opacity: 1, width: \"auto\" }}\n            exit={{ opacity: 0, width: 0 }}\n            className=\"ml-1 whitespace-nowrap text-xs mr-1 peer\"\n          >\n            Visit\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </motion.a>\n  );\n};\n\nconst AccordionItem = ({\n  role,\n  company,\n  description,\n  range,\n  skills,\n  link,\n}: {\n  role: string;\n  company: string;\n  description: string;\n  range: string;\n  skills?: string[];\n  link?: string;\n}) => {\n  // Short range for mobile view - not enough space\n  const shortRange = range.includes(\" - \")\n    ? range.split(\" - \")[1].trim()\n    : range?.trim() || \"\";\n\n  const [play] = useSound(\"/sounds/trigger.mp3\");\n\n  return (\n    <AccordionPrimitive.Item value={company}>\n      <AccordionPrimitive.Trigger\n        onClick={play}\n        className=\"flex justify-between items-center text-left w-full px-4 py-2 hover:bg-accent hover:text-white data-[state=open]:bg-accent/10 transition-all duration-150 hover:data-[state=open]:text-accent cursor-pointer group\"\n      >\n        <div className=\"flex items-center gap-x-1\">\n          <CaretRight\n            size={11}\n            className=\"text-accent group-data-[state=open]:rotate-90 transition duration-150 mr-1 group-hover:text-gray-1 group-focus-visible:text-gray-12 group-data-[state=open]:text-accent -ml-1\"\n            aria-hidden={true}\n          />\n          <span className=\"font-medium\">{company}</span>\n          <span className=\"whitespace-nowrap\">{role}</span>\n          {link && <ExpandingLink link={link} company={company} />}\n        </div>\n\n        <span className=\"hidden sm:block ml-auto\">{range}</span>\n        <span className=\"sm:hidden ml-auto text-sm\">{shortRange}</span>\n      </AccordionPrimitive.Trigger>\n      <AccordionPrimitive.Content className=\"overflow-clip transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down data-[state=open]:bg-accent/10\">\n        <div className=\"px-4 pb-4\">\n          <p>{description}</p>\n          <div className=\"flex gap-x-1.5\">\n            {skills?.map((skill) => {\n              return (\n                <div\n                  className=\"rounded-sm text-accent/70 text-sm mt-2\"\n                  key={skill}\n                >\n                  {skill}\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      </AccordionPrimitive.Content>\n    </AccordionPrimitive.Item>\n  );\n};\n\nconst Accordion = ({\n  className,\n  children,\n}: {\n  children: React.ReactNode;\n  className: string;\n}) => {\n  return (\n    <AccordionPrimitive.Root className={className} type=\"single\" collapsible>\n      {children}\n    </AccordionPrimitive.Root>\n  );\n};\n\nexport { Accordion, AccordionItem };\n"
  },
  {
    "path": "components/copy-email-button.tsx",
    "content": "\"use client\";\n\nimport { CopySimple } from \"@phosphor-icons/react\";\nimport { AnimatePresence, motion, type Variants } from \"motion/react\";\nimport { useState } from \"react\";\n\n//@ts-ignore\nimport useSound from \"use-sound\";\n\nconst motionVariants: Variants = {\n  initial: { y: 5, opacity: 0 },\n  animate: { y: 0, opacity: 1 },\n  exit: { y: -5, opacity: 0 },\n};\n\nexport const CopyEmailButton = () => {\n  const [play] = useSound(\"/sounds/copy.mp3\");\n  const [copied, setCopied] = useState(false);\n  const handleCopy = (text: string) => {\n    play();\n    setTimeout(() => {\n      setCopied(false);\n    }, 2000);\n    navigator.clipboard.writeText(text);\n    setCopied(true);\n  };\n  return (\n    <button\n      type=\"button\"\n      onClick={() => handleCopy(\"mitulxshah@gmail.com\")}\n      className=\"bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1 pr-1.5 rounded-[2px] cursor-pointer text-sm w-[56px]\"\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.div\n          className=\"flex gap-x-1.5 items-center justify-center\"\n          variants={motionVariants}\n          key={copied ? \"Copied!\" : \"Copy\"}\n          initial=\"initial\"\n          animate=\"animate\"\n          exit=\"exit\"\n          transition={{ duration: 0.05 }}\n        >\n          {copied ? (\n            \"Copied!\"\n          ) : (\n            <>\n              <CopySimple className=\"shrink-0\" size={12} aria-hidden={true} />\n              Copy\n            </>\n          )}\n        </motion.div>\n      </AnimatePresence>\n    </button>\n  );\n};\n\nexport const CopyEmailButtonAlt = () => {\n  const [play] = useSound(\"/sounds/copy.mp3\");\n  const [copied, setCopied] = useState(false);\n  const handleCopy = (text: string) => {\n    play();\n    setTimeout(() => {\n      setCopied(false);\n    }, 2000);\n    navigator.clipboard.writeText(text);\n    setCopied(true);\n  };\n\n  return (\n    <button\n      type=\"button\"\n      className=\"flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer\"\n      onClick={() => handleCopy(\"mitulxshah@gmail.com\")}\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.div\n          variants={motionVariants}\n          key={copied ? \"Copied!\" : \"Copy\"}\n          initial=\"initial\"\n          animate=\"animate\"\n          exit=\"exit\"\n          transition={{ duration: 0.05 }}\n        >\n          {copied ? \"Copied!\" : \"Email\"}\n        </motion.div>\n      </AnimatePresence>\n    </button>\n  );\n};\n"
  },
  {
    "path": "components/email-template.tsx",
    "content": "import type * as React from \"react\";\n\ninterface EmailTemplateProps {\n  entry: {\n    created_by: string;\n    entry: string;\n  };\n}\n\nexport const EmailTemplate: React.FC<EmailTemplateProps> = ({ entry }) => (\n  <div>\n    <h1>A new submission was made!</h1>\n    <p>\n      <strong>Name:</strong> {entry.created_by}\n    </p>\n    <p>\n      <strong>Email:</strong> {entry.entry}\n    </p>\n  </div>\n);\n"
  },
  {
    "path": "components/footer/footer-date.tsx",
    "content": "import { formatDate } from \"@/lib/utils\";\nimport Link from \"next/link\";\n\nconst FooterDate = async () => {\n  const data = await fetch(\n    \"https://api.github.com/repos/mitul-s/mitul.ca/commits\",\n    {\n      method: \"GET\",\n      headers: {\n        Accept: \"application/vnd.github.v3+json\",\n      },\n    }\n  ).then((res) => res.json());\n\n  // hack lazy way to bypass rate limit without going through auth\n  // to add proper stuff later!\n  const lastCommit = !data.message\n    ? data.map(\n        (commit: { commit: { committer: { date: string } } }) =>\n          commit.commit.committer.date\n      )[0]\n    : \"\";\n\n  const formattedDate = lastCommit\n    ? formatDate(new Date(lastCommit))\n    : \"2025/01/20\";\n\n  return (\n    <Link href=\"https://github.com/mitul-s/mitul.ca\" target=\"_blank\">\n      {formattedDate}\n    </Link>\n  );\n};\n\nexport default FooterDate;\n"
  },
  {
    "path": "components/footer/index.tsx",
    "content": "import { Suspense } from \"react\";\nimport FooterDate from \"./footer-date\";\n\nconst Footer = () => {\n  return (\n    <footer className=\"bg-accent text-gray-1 text-[12px] py-24\">\n      <div className=\"md:grid grid-cols-[160px_500px_auto]\">\n        <div className=\"grid grid-cols-[auto_1fr_auto] gap-x-4 items-center px-4 col-start-2\">\n          <span>\n            Last updated on{\" \"}\n            <Suspense>\n              <FooterDate />\n            </Suspense>\n          </span>\n          <div className=\"h-px bg-gray-2/40\" aria-hidden />\n          <p className=\"whitespace-nowrap\">With love, from Toronto, Canada.</p>\n        </div>\n      </div>\n    </footer>\n  );\n};\n\nexport default Footer;\n"
  },
  {
    "path": "components/gallery.tsx",
    "content": "import ScrollArea from \"@/components/scroll-area\";\nimport MorphingImageDialog from \"@/components/photo\";\n\nconst Gallery = ({\n  photos,\n}: {\n  photos: {\n    src: string;\n    alt: string;\n  }[];\n}) => {\n  return (\n    <ScrollArea className=\"relative md:w-full\">\n      <div className=\"flex w-full h-full gap-x-2 px-4\">\n        {photos.map((photo) => (\n          <MorphingImageDialog\n            key={photo.src}\n            src={photo.src}\n            alt={photo.alt}\n          />\n        ))}\n      </div>\n    </ScrollArea>\n  );\n};\n\nexport default Gallery;\n"
  },
  {
    "path": "components/json-ld.tsx",
    "content": "export function PersonJsonLd() {\n  const jsonLd = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Person\",\n    name: \"Mitul Shah\",\n    url: \"https://mitul.ca\",\n    image: \"https://mitul.ca/og-2.png\",\n    jobTitle: \"Design Engineer\",\n    worksFor: {\n      \"@type\": \"Organization\",\n      name: \"Vercel\",\n      url: \"https://vercel.com\",\n    },\n    description: \"Design engineer, photographer, and a bit more.\",\n    sameAs: [\n      \"https://twitter.com/typicalmitul\",\n      \"https://instagram.com/typicalmitul\",\n      \"https://github.com/mitul-s\",\n      \"https://typicalmitul.com\",\n    ],\n  };\n\n  return (\n    <script\n      type=\"application/ld+json\"\n      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}\n    />\n  );\n}\n\nexport function WebSiteJsonLd() {\n  const jsonLd = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebSite\",\n    name: \"Mitul Shah\",\n    url: \"https://mitul.ca\",\n    description: \"Personal portfolio and blog of Mitul Shah\",\n    author: {\n      \"@type\": \"Person\",\n      name: \"Mitul Shah\",\n    },\n  };\n\n  return (\n    <script\n      type=\"application/ld+json\"\n      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}\n    />\n  );\n}\n\nimport { getBlogPost } from \"@/lib/blog-posts\";\n\nconst BASE_URL = \"https://mitul.ca\";\n\ninterface BlogPostJsonLdProps {\n  slug: string;\n}\n\nexport function BlogPostJsonLd({ slug }: BlogPostJsonLdProps) {\n  const post = getBlogPost(slug);\n  if (!post) return null;\n\n  const jsonLd = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"BlogPosting\",\n    headline: post.title,\n    description: post.description,\n    url: `${BASE_URL}/p/${post.slug}`,\n    image: post.image || `${BASE_URL}/og-2.png`,\n    datePublished: post.datePublished,\n    dateModified: post.datePublished,\n    author: {\n      \"@type\": \"Person\",\n      name: \"Mitul Shah\",\n      url: BASE_URL,\n    },\n    publisher: {\n      \"@type\": \"Person\",\n      name: \"Mitul Shah\",\n      url: BASE_URL,\n    },\n  };\n\n  return (\n    <script\n      type=\"application/ld+json\"\n      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}\n    />\n  );\n}\n"
  },
  {
    "path": "components/link-primitive.tsx",
    "content": "import Link from \"next/link\";\nimport { cva } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nexport const link = cva([\"flex\", \"items-center\", \"gap-x-0.5\", \"w-fit\"], {\n  variants: {\n    variant: {\n      route: [\n        \"text-gray-11 text-sm hover:bg-accent hover:text-gray-12 px-1.5 py-1 rounded-sm -mx-1.5 font-medium border border-gray-12 hover:border-accent\",\n      ],\n      default: [\n        \"hover:bg-accent hover:text-gray-1 after:content-[''] after:absolute after:bottom-px after:left-0 after:w-full after:h-px after:bg-accent relative inline-flex\",\n      ],\n    },\n    popover: {\n      true: [\"bg-accent/20 mt-0.5\"],\n    },\n  },\n  defaultVariants: {\n    variant: \"default\",\n    popover: false,\n  },\n});\n\nconst LinkPrimitive = ({\n  href,\n  external,\n  className,\n  variant = \"default\",\n  popover,\n  children,\n  onClick,\n  ...props\n}: {\n  href: string;\n  external?: boolean;\n  className?: string;\n  variant?: \"default\" | \"route\";\n  popover?: boolean;\n  children: React.ReactNode;\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;\n}) => {\n  const Component = external ? \"a\" : Link;\n  return (\n    <Component\n      className={cn(link({ variant: variant, popover: popover }), className)}\n      target={external ? \"_blank\" : undefined}\n      href={href}\n      onClick={onClick}\n      {...props}\n    >\n      {children}\n    </Component>\n  );\n};\n\nexport default LinkPrimitive;\n"
  },
  {
    "path": "components/morphing-dialog.tsx",
    "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { motion, MotionConfig, AnimatePresence } from \"motion/react\";\nimport type { Transition, Variant } from \"motion/react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { X as XIcon } from \"@phosphor-icons/react\";\nimport useClickOutside from \"@/hooks/useClickOutside\";\nimport Image from \"next/image\";\n\nexport type MorphingDialogContextType = {\n  isOpen: boolean;\n  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  uniqueId: string;\n  triggerRef: React.RefObject<HTMLDivElement | null>;\n};\n\nconst MorphingDialogContext =\n  React.createContext<MorphingDialogContextType | null>(null);\n\nfunction useMorphingDialog() {\n  const context = useContext(MorphingDialogContext);\n  if (!context) {\n    throw new Error(\n      \"useMorphingDialog must be used within a MorphingDialogProvider\"\n    );\n  }\n  return context;\n}\n\nexport type MorphingDialogProviderProps = {\n  children: React.ReactNode;\n  transition?: Transition;\n};\n\nfunction MorphingDialogProvider({\n  children,\n  transition,\n}: MorphingDialogProviderProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const uniqueId = useId();\n  const triggerRef = useRef<HTMLDivElement>(null!);\n\n  const contextValue = useMemo(\n    () => ({\n      isOpen,\n      setIsOpen,\n      uniqueId,\n      triggerRef,\n    }),\n    [isOpen, uniqueId]\n  );\n\n  return (\n    <MorphingDialogContext.Provider value={contextValue}>\n      <MotionConfig transition={transition}>{children}</MotionConfig>\n    </MorphingDialogContext.Provider>\n  );\n}\n\nexport type MorphingDialogProps = {\n  children: React.ReactNode;\n  transition?: Transition;\n};\n\nfunction MorphingDialog({ children, transition }: MorphingDialogProps) {\n  return (\n    <MorphingDialogProvider>\n      <MotionConfig transition={transition}>{children}</MotionConfig>\n    </MorphingDialogProvider>\n  );\n}\n\nexport type MorphingDialogTriggerProps = {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n  triggerRef?: React.RefObject<HTMLDivElement | null>;\n};\n\nfunction MorphingDialogTrigger({\n  children,\n  className,\n  style,\n  triggerRef,\n}: MorphingDialogTriggerProps) {\n  const { setIsOpen, isOpen, uniqueId } = useMorphingDialog();\n\n  const handleClick = useCallback(() => {\n    setIsOpen(!isOpen);\n  }, [isOpen, setIsOpen]);\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent) => {\n      if (event.key === \"Enter\" || event.key === \" \") {\n        event.preventDefault();\n        setIsOpen(!isOpen);\n      }\n    },\n    [isOpen, setIsOpen]\n  );\n\n  return (\n    <motion.div\n      ref={triggerRef}\n      layoutId={`dialog-${uniqueId}`}\n      className={cn(\"relative cursor-pointer\", className)}\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n      style={style}\n      role=\"button\"\n      aria-haspopup=\"dialog\"\n      aria-expanded={isOpen}\n      aria-controls={`motion-ui-morphing-dialog-content-${uniqueId}`}\n      aria-label={`Open dialog ${uniqueId}`}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type MorphingDialogContentProps = {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n};\n\nfunction MorphingDialogContent({\n  children,\n  className,\n  style,\n}: MorphingDialogContentProps) {\n  const { setIsOpen, isOpen, uniqueId, triggerRef } = useMorphingDialog();\n  const containerRef = useRef<HTMLDivElement>(null!);\n  const [firstFocusableElement, setFirstFocusableElement] =\n    useState<HTMLElement | null>(null);\n  const [lastFocusableElement, setLastFocusableElement] =\n    useState<HTMLElement | null>(null);\n\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        setIsOpen(false);\n      }\n      if (event.key === \"Tab\") {\n        if (!firstFocusableElement || !lastFocusableElement) return;\n\n        if (event.shiftKey) {\n          if (document.activeElement === firstFocusableElement) {\n            event.preventDefault();\n            lastFocusableElement.focus();\n          }\n        } else {\n          if (document.activeElement === lastFocusableElement) {\n            event.preventDefault();\n            firstFocusableElement.focus();\n          }\n        }\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [setIsOpen, firstFocusableElement, lastFocusableElement]);\n\n  useEffect(() => {\n    if (isOpen) {\n      document.body.classList.add(\"overflow-hidden\");\n      const focusableElements = containerRef.current?.querySelectorAll(\n        'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n      );\n      if (focusableElements && focusableElements.length > 0) {\n        setFirstFocusableElement(focusableElements[0] as HTMLElement);\n        setLastFocusableElement(\n          focusableElements[focusableElements.length - 1] as HTMLElement\n        );\n        (focusableElements[0] as HTMLElement).focus();\n      }\n    } else {\n      document.body.classList.remove(\"overflow-hidden\");\n      triggerRef.current?.focus();\n    }\n  }, [isOpen, triggerRef]);\n\n  useClickOutside(containerRef, () => {\n    if (isOpen) {\n      setIsOpen(false);\n    }\n  });\n\n  return (\n    <motion.div\n      ref={containerRef}\n      layoutId={`dialog-${uniqueId}`}\n      className={cn(\"overflow-hidden\", className)}\n      style={style}\n      role=\"dialog\"\n      aria-modal=\"true\"\n      aria-labelledby={`motion-ui-morphing-dialog-title-${uniqueId}`}\n      aria-describedby={`motion-ui-morphing-dialog-description-${uniqueId}`}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type MorphingDialogContainerProps = {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n};\n\nfunction MorphingDialogContainer({ children }: MorphingDialogContainerProps) {\n  const { isOpen, uniqueId } = useMorphingDialog();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n    return () => setMounted(false);\n  }, []);\n\n  if (!mounted) return null;\n\n  return createPortal(\n    <AnimatePresence initial={false} mode=\"sync\">\n      {isOpen && (\n        <>\n          <motion.div\n            key={`backdrop-${uniqueId}`}\n            className=\"fixed inset-0 h-full w-full bg-white/95 z-50\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n          />\n          <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n            {children}\n          </div>\n        </>\n      )}\n    </AnimatePresence>,\n    document.body\n  );\n}\n\nexport type MorphingDialogTitleProps = {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n};\n\nfunction MorphingDialogTitle({\n  children,\n  className,\n  style,\n}: MorphingDialogTitleProps) {\n  const { uniqueId } = useMorphingDialog();\n\n  return (\n    <motion.div\n      layoutId={`dialog-title-container-${uniqueId}`}\n      className={className}\n      style={style}\n      layout\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type MorphingDialogSubtitleProps = {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n};\n\nfunction MorphingDialogSubtitle({\n  children,\n  className,\n  style,\n}: MorphingDialogSubtitleProps) {\n  const { uniqueId } = useMorphingDialog();\n\n  return (\n    <motion.div\n      layoutId={`dialog-subtitle-container-${uniqueId}`}\n      className={className}\n      style={style}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type MorphingDialogDescriptionProps = {\n  children: React.ReactNode;\n  className?: string;\n  disableLayoutAnimation?: boolean;\n  variants?: {\n    initial: Variant;\n    animate: Variant;\n    exit: Variant;\n  };\n};\n\nfunction MorphingDialogDescription({\n  children,\n  className,\n  variants,\n  disableLayoutAnimation,\n}: MorphingDialogDescriptionProps) {\n  const { uniqueId } = useMorphingDialog();\n\n  return (\n    <motion.div\n      key={`dialog-description-${uniqueId}`}\n      layoutId={\n        disableLayoutAnimation\n          ? undefined\n          : `dialog-description-content-${uniqueId}`\n      }\n      variants={variants}\n      className={className}\n      initial=\"initial\"\n      animate=\"animate\"\n      exit=\"exit\"\n      id={`dialog-description-${uniqueId}`}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type MorphingDialogImageProps = {\n  src: string;\n  alt: string;\n  className?: string;\n  style?: React.CSSProperties;\n};\n\nconst MotionImage = motion(Image);\n\nfunction MorphingDialogImage({\n  src,\n  alt,\n  className,\n  style,\n}: MorphingDialogImageProps) {\n  const { uniqueId } = useMorphingDialog();\n\n  return (\n    <MotionImage\n      src={src}\n      alt={alt}\n      width={1000}\n      height={1000}\n      className={cn(\"object-cover object-center w-full h-full\", className)}\n      layoutId={`dialog-img-${uniqueId}`}\n      style={style}\n      quality={40}\n    />\n  );\n}\n\nexport type MorphingDialogCloseProps = {\n  children?: React.ReactNode;\n  className?: string;\n  variants?: {\n    initial: Variant;\n    animate: Variant;\n    exit: Variant;\n  };\n};\n\nfunction MorphingDialogClose({\n  children,\n  className,\n  variants,\n}: MorphingDialogCloseProps) {\n  const { setIsOpen, uniqueId } = useMorphingDialog();\n\n  const handleClose = useCallback(() => {\n    setIsOpen(false);\n  }, [setIsOpen]);\n\n  return (\n    <motion.button\n      onClick={handleClose}\n      type=\"button\"\n      aria-label=\"Close dialog\"\n      key={`dialog-close-${uniqueId}`}\n      className={cn(\"absolute right-6 top-6\", className)}\n      initial=\"initial\"\n      animate=\"animate\"\n      exit=\"exit\"\n      variants={variants}\n    >\n      {children || <XIcon size={24} />}\n    </motion.button>\n  );\n}\n\nexport {\n  MorphingDialog,\n  MorphingDialogTrigger,\n  MorphingDialogContainer,\n  MorphingDialogContent,\n  MorphingDialogClose,\n  MorphingDialogTitle,\n  MorphingDialogSubtitle,\n  MorphingDialogDescription,\n  MorphingDialogImage,\n};\n"
  },
  {
    "path": "components/music-player.tsx",
    "content": "import getLastPlayed from \"@/lib/spotify\";\nimport Image from \"next/image\";\nimport { getShelves } from \"@/lib/literal\";\nimport NowPlayingClient from \"./now-playing-client\";\nimport Link from \"next/link\";\nimport { cacheLife } from \"next/cache\";\n\nconst MusicPlayer = async () => {\n  \"use cache\";\n  cacheLife(\"minutes\");\n  const { data: song } = await getLastPlayed();\n  const { reading } = await getShelves();\n\n  return (\n    <div className=\"grid grid-cols-2 gap-x-4 gap-y-4 my-4\">\n      <NowPlayingClient initial={song} />\n      <Link\n        href={`https://literal.club/ms/book/${reading.slug}`}\n        target=\"_blank\"\n        className=\"flex flex-row items-center gap-x-1.5 w-fit overflow-hidden group\"\n      >\n        <div className=\"rounded-md border border-gray-6 h-16 w-16 aspect-square relative\">\n          <Image\n            src={reading.cover}\n            fill\n            alt=\"Album cover\"\n            className=\"object-cover\"\n            quality={40}\n          />\n        </div>\n        <div className=\"flex flex-col gap-y-1 justify-center leading-none\">\n          <span className=\"font-medium text-accent truncate max-w-20 min-md:max-w-32 group-hover:underline\">\n            {reading.title}\n          </span>\n          <span className=\"text-sm\">{reading.author}</span>\n        </div>\n      </Link>\n    </div>\n  );\n};\n\nexport default MusicPlayer;\n"
  },
  {
    "path": "components/now-playing-client.tsx",
    "content": "\"use client\";\n\nimport useSWR from \"swr\";\nimport Image from \"next/image\";\nimport Filter from \"bad-words\";\n\nconst fetcher = (url: string) =>\n  fetch(url, { cache: \"no-store\" }).then((r) => r.json());\n\nexport default function NowPlayingClient({ initial }: { initial: any }) {\n  const { data } = useSWR(\"/api/spotify\", fetcher, {\n    refreshInterval: 60000,\n    fallbackData: initial,\n    revalidateOnFocus: true,\n  });\n\n  if (!data) return null;\n  const song = data?.data || initial;\n\n  const recent = song.is_playing ? song.item : song.items[0].track;\n\n  if (!recent)\n    return (\n      <div className=\"h-16 w-full\">\n        im probably being rate limited by spotify if u see this\n      </div>\n    );\n\n  const filter = new Filter();\n\n  const track = {\n    title: filter.clean(recent.name),\n    artist: recent.artists\n      .map((_artist: { name: string }) => _artist.name)\n      .shift(),\n    songUrl: recent.external_urls.spotify,\n    coverArt: recent.album.images[0].url,\n    previewUrl: recent.preview_url,\n  };\n\n  return (\n    <div className=\"flex flex-row items-center gap-x-1.5 w-fit overflow-hidden\">\n      <div className=\"rounded-md border border-gray-6 h-16 w-16 aspect-square relative\">\n        <Image\n          src={track.coverArt}\n          fill\n          alt=\"Album cover\"\n          className=\"object-cover\"\n          quality={40}\n        />\n      </div>\n      <div className=\"flex flex-col gap-y-1 justify-center leading-none\">\n        <span className=\"font-medium text-accent\">{track.title}</span>\n        <span className=\"text-sm\">{track.artist}</span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/photo.tsx",
    "content": "\"use client\";\n\nimport {\n  MorphingDialog,\n  MorphingDialogTrigger,\n  MorphingDialogContent,\n  MorphingDialogClose,\n  MorphingDialogImage,\n  MorphingDialogContainer,\n} from \"@/components/morphing-dialog\";\nimport { X as XIcon } from \"@phosphor-icons/react\";\n\nexport function MorphingImageDialog({\n  src,\n  alt,\n}: {\n  src: string;\n  alt: string;\n}) {\n  return (\n    <MorphingDialog\n      transition={{\n        duration: 0.3,\n        ease: \"easeInOut\",\n      }}\n    >\n      <MorphingDialogTrigger className=\"relative overflow-hidden rounded-4 w-60 h-80 shrink-0 shadow border border-accent/50\">\n        <MorphingDialogImage\n          src={src}\n          alt={alt}\n          className=\"object-cover object-center w-full h-full\"\n        />\n      </MorphingDialogTrigger>\n      <MorphingDialogContainer>\n        <MorphingDialogContent className=\"relative\">\n          <MorphingDialogImage\n            src={src}\n            alt={alt}\n            className=\"h-auto w-full max-w-[90vw] rounded-[4px] object-cover lg:h-[90vh]\"\n          />\n        </MorphingDialogContent>\n        <MorphingDialogClose\n          className=\"fixed right-6 top-6 h-fit w-fit rounded-full bg-white p-1\"\n          variants={{\n            initial: { opacity: 0 },\n            animate: {\n              opacity: 1,\n              transition: { delay: 0.3, duration: 0.1 },\n            },\n            exit: { opacity: 0, transition: { duration: 0 } },\n          }}\n        >\n          <XIcon className=\"h-5 w-5 text-zinc-500\" />\n        </MorphingDialogClose>\n      </MorphingDialogContainer>\n    </MorphingDialog>\n  );\n}\n\nexport default MorphingImageDialog;\n"
  },
  {
    "path": "components/scroll-area.tsx",
    "content": "\"use client\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nexport default function ScrollArea({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <ScrollAreaPrimitive.Root type=\"always\" className={className}>\n      <ScrollAreaPrimitive.Viewport className=\"w-full h-full\">\n        {children}\n        <ScrollAreaPrimitive.Scrollbar\n          className=\"flex select-none -mb-2 w-[calc(100%-32px)] mx-auto rounded-md touch-none bg-gray-6 transition-all duration-100 ease-out data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-0.5 hover:data-[orientation=horizontal]:h-1 group\"\n          orientation=\"horizontal\"\n        >\n          <ScrollAreaPrimitive.Thumb className=\"flex-1 bg-accent/50 group-hover:bg-accent rounded-md relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]\" />\n        </ScrollAreaPrimitive.Scrollbar>\n      </ScrollAreaPrimitive.Viewport>\n    </ScrollAreaPrimitive.Root>\n  );\n}\n"
  },
  {
    "path": "components/section.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nconst Section = ({\n  heading,\n  children,\n  className,\n}: {\n  heading?: string;\n  className?: string;\n  children: React.ReactNode;\n}) => {\n  return (\n    <div className={cn(\"p-1 md:p-4\", className)}>\n      {heading && <h2 className=\"mb-2 font-medium text-gray-11\">{heading}</h2>}\n      {children}\n    </div>\n  );\n};\n\nexport default Section;\n"
  },
  {
    "path": "components/shader.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\n// --- WebGL helpers -----------------------------------------------------------\nfunction createShader(gl: WebGLRenderingContext, type: number, source: string) {\n  const shader = gl.createShader(type)!;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    const info = gl.getShaderInfoLog(shader);\n    gl.deleteShader(shader);\n    throw new Error(\"Shader compile failed: \" + info);\n  }\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGLRenderingContext,\n  vsSource: string,\n  fsSource: string\n) {\n  const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);\n  const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);\n  const program = gl.createProgram()!;\n  gl.attachShader(program, vs);\n  gl.attachShader(program, fs);\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    const info = gl.getProgramInfoLog(program);\n    gl.deleteProgram(program);\n    throw new Error(\"Program link failed: \" + info);\n  }\n  gl.deleteShader(vs);\n  gl.deleteShader(fs);\n  return program;\n}\n\n// --- Shaders -----------------------------------------------------------------\nconst VERT = `\nattribute vec2 aPos;\nvarying vec2 vUV;\nvoid main() {\n  vUV = aPos * 0.5 + 0.5;\n  gl_Position = vec4(aPos, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `\nprecision highp float;\n\nvarying vec2 vUV;\nuniform sampler2D uTex;\nuniform vec2 uResolution;\nuniform vec2 uVideoSize;\nuniform float uDotSize;\nuniform float uAngle;\nuniform vec3 uInkColor;\n\nvec2 coverMap(vec2 uv, vec2 canvasSize, vec2 contentSize) {\n  vec2 pos = (uv - 0.5) * canvasSize;\n  float s = max(canvasSize.x / contentSize.x, canvasSize.y / contentSize.y);\n  vec2 src = pos / s + 0.5 * contentSize;\n  return src / contentSize;\n}\n\nvec2 rotate2D(vec2 p, float a) {\n  float s = sin(a), c = cos(a);\n  return mat2(c, -s, s, c) * p;\n}\n\nvoid main() {\n  vec2 uv = coverMap(vUV, uResolution, uVideoSize);\n  vec3 vid = texture2D(uTex, uv).rgb;\n\n  float lum = dot(vid, vec3(0.299, 0.587, 0.114));\n\n  vec2 p = gl_FragCoord.xy - 0.5 * uResolution;\n  p = rotate2D(p, uAngle);\n  p += 0.5 * uResolution;\n\n  vec2 g = p / max(uDotSize, 1.0);\n  vec2 cell = fract(g) - 0.5;\n\n  float radius = (1.0 - lum) * 0.5;\n  float dist = length(cell);\n\n  float feather = 0.35 / max(uDotSize, 1.0);\n  float dotMask = 1.0 - smoothstep(radius, radius + feather, dist);\n\n  gl_FragColor = vec4(uInkColor, dotMask);\n}\n`;\n\n// Fixed color (keeps perf; avoids per-frame style reads)\nconst INK_COLOR: [number, number, number] = [2 / 255, 16 / 255, 147 / 255];\n\nconst prefersReducedMotion = () => {\n  if (typeof window === \"undefined\") return false;\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n};\n\nexport default function DitherShaderCanvas() {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const videoRef = useRef<HTMLVideoElement | null>(null);\n\n  const [fallback, setFallback] = useState(false);\n  const [ready, setReady] = useState(false); // <- canvas will fade in after first GPU upload\n  const didFirstDrawRef = useRef(false);\n\n  // Tunables\n  const DOT_SIZE = 5;\n  const ANGLE_DEG = 68;\n  const RES_SCALE = 0.75;\n  const FPS_CAP_NO_RVFC = 30;\n\n  // GL refs\n  const rafRef = useRef<number | null>(null);\n  const glRef = useRef<WebGLRenderingContext | null>(null);\n  const programRef = useRef<WebGLProgram | null>(null);\n  const bufRef = useRef<WebGLBuffer | null>(null);\n  const textureRef = useRef<WebGLTexture | null>(null);\n\n  const uTexRef = useRef<WebGLUniformLocation | null>(null);\n  const uResolutionRef = useRef<WebGLUniformLocation | null>(null);\n  const uVideoSizeRef = useRef<WebGLUniformLocation | null>(null);\n  const uDotSizeRef = useRef<WebGLUniformLocation | null>(null);\n  const uAngleRef = useRef<WebGLUniformLocation | null>(null);\n  const uInkColorRef = useRef<WebGLUniformLocation | null>(null);\n\n  // Render gating\n  const hasRVFCRef = useRef(false);\n  const needsUploadRef = useRef(false);\n  const drawPendingRef = useRef(false);\n  const lastTimeRef = useRef(-1);\n  const lastDrawMsRef = useRef(0);\n\n  // Visibility gating\n  const isVisibleRef = useRef(true);\n  const isDocVisibleRef = useRef(\n    typeof document !== \"undefined\" ? !document.hidden : true\n  );\n  const isActive = () => isVisibleRef.current && isDocVisibleRef.current;\n\n  const scheduleRender = () => {\n    if (drawPendingRef.current) return;\n    drawPendingRef.current = true;\n    rafRef.current = requestAnimationFrame(drawOnce);\n  };\n\n  const setSize = () => {\n    const canvas = canvasRef.current;\n    const gl = glRef.current;\n    if (!canvas || !gl) return;\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const scale = RES_SCALE;\n    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr * scale));\n    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr * scale));\n    if (canvas.width !== w || canvas.height !== h) {\n      canvas.width = w;\n      canvas.height = h;\n      gl.viewport(0, 0, w, h);\n      scheduleRender();\n    }\n  };\n\n  const drawOnce = () => {\n    drawPendingRef.current = false;\n    const gl = glRef.current;\n    const v = videoRef.current;\n    const program = programRef.current;\n    const canvas = canvasRef.current;\n    if (!gl || !program || !v || !canvas) return;\n    if (!isActive()) return;\n\n    if (!hasRVFCRef.current) {\n      const now = performance.now();\n      const minDelta = 1000 / FPS_CAP_NO_RVFC;\n      if (now - lastDrawMsRef.current < minDelta) {\n        drawPendingRef.current = true;\n        rafRef.current = requestAnimationFrame(drawOnce);\n        return;\n      }\n      lastDrawMsRef.current = now;\n    }\n\n    gl.useProgram(program);\n\n    if (uResolutionRef.current)\n      gl.uniform2f(uResolutionRef.current, canvas.width, canvas.height);\n    const vw = v.videoWidth || 1920;\n    const vh = v.videoHeight || 1080;\n    if (uVideoSizeRef.current) gl.uniform2f(uVideoSizeRef.current, vw, vh);\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    if (uDotSizeRef.current)\n      gl.uniform1f(uDotSizeRef.current, Math.max(1, DOT_SIZE) * dpr);\n    if (uAngleRef.current)\n      gl.uniform1f(uAngleRef.current, (ANGLE_DEG * Math.PI) / 180.0);\n\n    // Detect new frames if no RVFC\n    if (!hasRVFCRef.current && v.readyState >= 2) {\n      const t = v.currentTime;\n      if (t !== lastTimeRef.current) {\n        needsUploadRef.current = true;\n        lastTimeRef.current = t;\n      }\n    }\n\n    let didUpload = false;\n    if (needsUploadRef.current && v.readyState >= 2) {\n      const texture = textureRef.current!;\n      gl.bindTexture(gl.TEXTURE_2D, texture);\n      try {\n        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, v);\n        needsUploadRef.current = false;\n        didUpload = true;\n      } catch {\n        setFallback(true);\n        return;\n      }\n    }\n\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n\n    // Mark ready immediately after the first successful GPU upload + draw\n    if (didUpload && !didFirstDrawRef.current) {\n      didFirstDrawRef.current = true;\n      // Defer state change to next macrotask to ensure the first frame is painted\n      setTimeout(() => setReady(true), 0);\n    }\n\n    if (!hasRVFCRef.current && isActive()) {\n      drawPendingRef.current = true;\n      rafRef.current = requestAnimationFrame(drawOnce);\n    }\n  };\n\n  // Init video\n  useEffect(() => {\n    const v = videoRef.current;\n    if (!v) return;\n    v.muted = true;\n    v.defaultMuted = true;\n\n    const START_TIME = 5;\n\n    v.currentTime = START_TIME;\n\n    const handleEnded = () => {\n      v.currentTime = START_TIME;\n      v.play().catch(() => {});\n    };\n\n    v.addEventListener(\"ended\", handleEnded);\n\n    const tryPlay = () => v.play().catch(() => {});\n    tryPlay();\n\n    if (v.readyState >= 2) {\n      needsUploadRef.current = true;\n      scheduleRender();\n    } else {\n      const onLoadedData = () => {\n        needsUploadRef.current = true;\n        tryPlay();\n        scheduleRender();\n      };\n      v.addEventListener(\"loadeddata\", onLoadedData, { once: true });\n      return () => {\n        v.removeEventListener(\"loadeddata\", onLoadedData);\n        v.removeEventListener(\"ended\", handleEnded);\n      };\n    }\n\n    return () => {\n      v.removeEventListener(\"ended\", handleEnded);\n    };\n  }, []);\n\n  // Main GL setup\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const video = videoRef.current;\n    if (!canvas || !video) return;\n\n    const gl = canvas.getContext(\"webgl\", {\n      alpha: true,\n      antialias: false,\n      depth: false,\n      stencil: false,\n      premultipliedAlpha: true,\n      preserveDrawingBuffer: false,\n      desynchronized: true,\n      powerPreference: \"low-power\",\n    });\n    if (!gl) {\n      setFallback(true);\n      return;\n    }\n    glRef.current = gl;\n\n    gl.disable(gl.DITHER);\n    gl.disable(gl.DEPTH_TEST);\n    gl.disable(gl.CULL_FACE);\n    gl.disable(gl.SCISSOR_TEST);\n    gl.enable(gl.BLEND);\n    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\n    const program = createProgram(gl, VERT, FRAG);\n    programRef.current = program;\n    gl.useProgram(program);\n\n    // Full-screen single triangle\n    const tri = new Float32Array([-1, -1, 3, -1, -1, 3]);\n    const buf = gl.createBuffer();\n    bufRef.current = buf;\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.bufferData(gl.ARRAY_BUFFER, tri, gl.STATIC_DRAW);\n    const aPos = gl.getAttribLocation(program, \"aPos\");\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n\n    // Uniforms\n    uTexRef.current = gl.getUniformLocation(program, \"uTex\");\n    uResolutionRef.current = gl.getUniformLocation(program, \"uResolution\");\n    uVideoSizeRef.current = gl.getUniformLocation(program, \"uVideoSize\");\n    uDotSizeRef.current = gl.getUniformLocation(program, \"uDotSize\");\n    uAngleRef.current = gl.getUniformLocation(program, \"uAngle\");\n    uInkColorRef.current = gl.getUniformLocation(program, \"uInkColor\");\n    if (uInkColorRef.current)\n      gl.uniform3f(\n        uInkColorRef.current,\n        INK_COLOR[0],\n        INK_COLOR[1],\n        INK_COLOR[2]\n      );\n\n    // Texture\n    const texture = gl.createTexture();\n    textureRef.current = texture;\n    gl.activeTexture(gl.TEXTURE0);\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    if (uTexRef.current) gl.uniform1i(uTexRef.current, 0);\n    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);\n\n    // Seed transparent pixel\n    const pixel = new Uint8Array([0, 0, 0, 0]);\n    gl.texImage2D(\n      gl.TEXTURE_2D,\n      0,\n      gl.RGBA,\n      1,\n      1,\n      0,\n      gl.RGBA,\n      gl.UNSIGNED_BYTE,\n      pixel\n    );\n\n    // Size / viewport\n    setSize();\n    const onResize = () => setSize();\n    window.addEventListener(\"resize\", onResize, { passive: true });\n\n    // Prefer requestVideoFrameCallback\n    const setupRVFC = () => {\n      const vv: any = video;\n      if (vv && typeof vv.requestVideoFrameCallback === \"function\") {\n        hasRVFCRef.current = true;\n        let id: any;\n        const onFrame = () => {\n          if (!isActive()) return;\n          needsUploadRef.current = true;\n          scheduleRender();\n          id = vv.requestVideoFrameCallback(onFrame);\n        };\n        id = vv.requestVideoFrameCallback(onFrame);\n        return () => vv.cancelVideoFrameCallback?.(id);\n      }\n      hasRVFCRef.current = false;\n      scheduleRender();\n      return () => {};\n    };\n    const cancelRVFC = setupRVFC();\n\n    const onCanPlay = () => {\n      needsUploadRef.current = true;\n      video.play().catch(() => {});\n      scheduleRender();\n    };\n    video.addEventListener(\"canplay\", onCanPlay, { once: true });\n\n    const onLost = (e: Event) => {\n      e.preventDefault();\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      setFallback(true);\n    };\n    canvas.addEventListener(\"webglcontextlost\", onLost as any, {\n      passive: false,\n    });\n\n    return () => {\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      window.removeEventListener(\"resize\", onResize);\n      video.removeEventListener(\"canplay\", onCanPlay);\n      canvas.removeEventListener(\"webglcontextlost\", onLost as any);\n      cancelRVFC();\n\n      const gl2 = glRef.current;\n      const program2 = programRef.current;\n      const buf2 = bufRef.current;\n      const texture2 = textureRef.current;\n      if (gl2) {\n        if (texture2) gl2.deleteTexture(texture2);\n        if (buf2) gl2.deleteBuffer(buf2);\n        if (program2) gl2.deleteProgram(program2);\n      }\n      glRef.current = null;\n      programRef.current = null;\n      bufRef.current = null;\n      textureRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Pause when off-screen\n  useEffect(() => {\n    const root = containerRef.current;\n    if (!root) return;\n\n    const obs = new IntersectionObserver(\n      (entries) => {\n        const entry = entries[0];\n        isVisibleRef.current = !!entry?.isIntersecting;\n        const v = videoRef.current;\n        if (isActive()) {\n          v?.play().catch(() => {});\n          scheduleRender();\n        } else {\n          v?.pause();\n        }\n      },\n      { root: null, threshold: 0.01 }\n    );\n    obs.observe(root);\n    return () => obs.disconnect();\n  }, []);\n\n  // Pause when tab hidden\n  useEffect(() => {\n    const onVis = () => {\n      isDocVisibleRef.current = !document.hidden;\n      const v = videoRef.current;\n      if (isActive()) {\n        v?.play().catch(() => {});\n        scheduleRender();\n      } else {\n        v?.pause();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n    return () => document.removeEventListener(\"visibilitychange\", onVis);\n  }, []);\n\n  const reducedMotion = prefersReducedMotion();\n  if (reducedMotion) {\n    return <></>;\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"absolute -z-10 inset-0 w-full h-full overflow-hidden pointer-events-none\"\n    >\n      {/* Show the raw video until the canvas has its first uploaded frame (or on fallback) */}\n      <video\n        ref={videoRef}\n        src={\n          \"https://inqeleafibjx2dzc.public.blob.vercel-storage.com/main/shader-vid.mp4\"\n        }\n        muted\n        autoPlay={!reducedMotion}\n        // loop\n        playsInline\n        preload=\"metadata\"\n        aria-hidden=\"true\"\n        crossOrigin=\"anonymous\"\n        className={cn(\n          \"absolute inset-0 w-full h-full object-cover transition-opacity duration-200 pointer-events-none\",\n          // TODO tweak? framer motion?\n          !ready ? \"opacity-100\" : \"opacity-0\",\n          fallback ? \"opacity-0\" : \"opacity-0\"\n        )}\n      />\n\n      {/* Fade the shader canvas in only after first draw to avoid flash */}\n      <canvas\n        ref={canvasRef}\n        className={\n          fallback\n            ? \"hidden\"\n            : `absolute inset-0 w-full h-full block pointer-events-none transition-opacity duration-200 ${\n                ready ? \"opacity-70\" : \"opacity-0\"\n              }`\n        }\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/theme-switcher.tsx",
    "content": "\"use client\";\n\nimport { PaintRoller } from \"@phosphor-icons/react\";\nimport { useTheme } from \"next-themes\";\n\nconst ThemeChanger = () => {\n  const { theme, setTheme } = useTheme();\n  const themes = [\"blue\", \"green\", \"red\"];\n\n  const toggleTheme = () => {\n    const currentThemeIndex = themes.indexOf(theme ?? themes[0]);\n    const nextThemeIndex = (currentThemeIndex + 1) % themes.length;\n    setTheme(themes[nextThemeIndex]);\n  };\n\n  return (\n    <div>\n      <button\n        type=\"button\"\n        onClick={toggleTheme}\n        className=\"flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer h-full\"\n        aria-label=\"Change theme\"\n      >\n        <PaintRoller\n          className=\"shrink-0\"\n          size={12}\n          aria-hidden={true}\n          weight=\"fill\"\n        />\n      </button>\n    </div>\n  );\n};\n\nexport default ThemeChanger;\n"
  },
  {
    "path": "components/tree-client.tsx",
    "content": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\nconst P5AsciiTree = dynamic(() => import(\"@/components/tree\"), {\n  ssr: false,\n  loading: () => <div className=\"w-[900px] h-[720px]\" />,\n});\n\nconst TreeClient = () => {\n  return <P5AsciiTree />;\n};\n\nexport default TreeClient;\n"
  },
  {
    "path": "components/tree.tsx",
    "content": "// @ts-nocheck\n\n\"use client\";\nimport type React from \"react\";\nimport { useRef, useEffect, useState } from \"react\";\nimport { motion } from \"motion/react\";\nimport p5 from \"p5\";\n\ninterface Particle {\n  x: number;\n  y: number;\n  originalX: number;\n  originalY: number;\n  vx: number;\n  vy: number;\n}\n\nconst P5AsciiTree: React.FC = () => {\n  const sketchRef = useRef<HTMLDivElement>(null);\n  const [ready, setReady] = useState(false);\n  const readyRef = useRef(false);\n\n  useEffect(() => {\n    if (!sketchRef.current) return;\n    const containerEl = sketchRef.current;\n    containerEl.style.visibility = \"hidden\";\n    let signaledReady = false;\n\n    const sketch = (p: p5) => {\n      let img: p5.Image;\n      const density = \"⋅\";\n      let particles: Particle[] = [];\n      const maxSpeed = 0.5;\n      const influenceRadius = 60;\n      let quadtree: any;\n      let prevMouseX: number;\n      let prevMouseY: number;\n\n      p.preload = () => {\n        img = p.loadImage(\n          \"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Tree%20PNG%203498-WZXyiQf0MZdOkGZGNuQvBFOgvMHxgZ.png\"\n          // \"https://inqeleafibjx2dzc.public.blob.vercel-storage.com/Tree%20PNG%20Image-FuCcTHTGIQhPV1Q9zND4BeFX4yuyn2.png\"\n        );\n      };\n\n      class QuadTree {\n        boundary: { x: number; y: number; w: number; h: number };\n        capacity: number;\n        points: Particle[];\n        divided: boolean;\n        northwest: QuadTree | null;\n        northeast: QuadTree | null;\n        southwest: QuadTree | null;\n        southeast: QuadTree | null;\n\n        constructor(\n          boundary: { x: number; y: number; w: number; h: number },\n          capacity: number\n        ) {\n          this.boundary = boundary;\n          this.capacity = capacity;\n          this.points = [];\n          this.divided = false;\n          this.northwest = null;\n          this.northeast = null;\n          this.southwest = null;\n          this.southeast = null;\n        }\n\n        insert(point: Particle) {\n          if (!this.contains(point)) {\n            return false;\n          }\n\n          if (this.points.length < this.capacity) {\n            this.points.push(point);\n            return true;\n          }\n\n          if (!this.divided) {\n            this.subdivide();\n          }\n\n          return (\n            this.northwest!.insert(point) ||\n            this.northeast!.insert(point) ||\n            this.southwest!.insert(point) ||\n            this.southeast!.insert(point)\n          );\n        }\n\n        subdivide() {\n          const x = this.boundary.x;\n          const y = this.boundary.y;\n          const w = this.boundary.w / 2;\n          const h = this.boundary.h / 2;\n\n          this.northwest = new QuadTree(\n            { x: x, y: y, w: w, h: h },\n            this.capacity\n          );\n          this.northeast = new QuadTree(\n            { x: x + w, y: y, w: w, h: h },\n            this.capacity\n          );\n          this.southwest = new QuadTree(\n            { x: x, y: y + h, w: w, h: h },\n            this.capacity\n          );\n          this.southeast = new QuadTree(\n            { x: x + w, y: y + h, w: w, h: h },\n            this.capacity\n          );\n\n          this.divided = true;\n        }\n\n        contains(point: Particle) {\n          return (\n            point.x >= this.boundary.x &&\n            point.x < this.boundary.x + this.boundary.w &&\n            point.y >= this.boundary.y &&\n            point.y < this.boundary.y + this.boundary.h\n          );\n        }\n\n        query(range: { x: number; y: number; r: number }, found: Particle[]) {\n          if (!this.intersects(range)) {\n            return;\n          }\n\n          for (let p of this.points) {\n            if (this.inCircle(range, p)) {\n              found.push(p);\n            }\n          }\n\n          if (this.divided) {\n            this.northwest!.query(range, found);\n            this.northeast!.query(range, found);\n            this.southwest!.query(range, found);\n            this.southeast!.query(range, found);\n          }\n        }\n\n        intersects(range: { x: number; y: number; r: number }) {\n          return !(\n            range.x - range.r > this.boundary.x + this.boundary.w ||\n            range.x + range.r < this.boundary.x ||\n            range.y - range.r > this.boundary.y + this.boundary.h ||\n            range.y + range.r < this.boundary.y\n          );\n        }\n\n        inCircle(range: { x: number; y: number; r: number }, point: Particle) {\n          let dx = range.x - point.x;\n          let dy = range.y - point.y;\n          return dx * dx + dy * dy <= range.r * range.r;\n        }\n      }\n\n      p.setup = () => {\n        p.createCanvas(900, 720);\n        p.clear();\n        p.textSize(16);\n        p.textAlign(p.CENTER, p.CENTER);\n\n        img.loadPixels();\n        const stepSize = 6;\n        const w = p.width / img.width;\n        const h = p.height / img.height;\n\n        // Define the area to exclude (top-left corner)\n        const excludeWidth = img.width * 0.2; // Exclude 20% from the left\n        const excludeHeight = img.height * 0.2; // Exclude 20% from the top\n\n        for (let i = 0; i < img.width; i += stepSize) {\n          for (let j = 0; j < img.height; j += stepSize) {\n            // Skip particles in the top-left corner\n            if (i < excludeWidth && j < excludeHeight) continue;\n\n            const pixelIndex = (i + j * img.width) * 4;\n            const r = img.pixels[pixelIndex];\n            const g = img.pixels[pixelIndex + 1];\n            const b = img.pixels[pixelIndex + 2];\n            const brightness = (r + g + b) / 3;\n\n            if (brightness > 50) {\n              particles.push({\n                x: i * w,\n                y: j * h,\n                originalX: i * w,\n                originalY: j * h,\n                vx: 0,\n                vy: 0,\n              });\n            }\n          }\n        }\n\n        quadtree = new QuadTree({ x: 0, y: 0, w: p.width, h: p.height }, 4);\n        prevMouseX = p.mouseX;\n        prevMouseY = p.mouseY;\n      };\n\n      p.draw = () => {\n        p.clear();\n\n        // Only access document on the client side\n        let r = 2,\n          g = 16,\n          b = 147; // Default fallback values\n        if (typeof document !== \"undefined\") {\n          const colorString = getComputedStyle(document.documentElement)\n            .getPropertyValue(\"--color-accent-rgb\")\n            .trim();\n\n          const colorValues = colorString\n            .split(\",\")\n            .map((str) => Number.parseInt(str.trim(), 10));\n\n          if (colorValues.length === 3 && !colorValues.some(isNaN)) {\n            [r, g, b] = colorValues;\n          }\n        }\n\n        p.fill(r, g, b);\n        // p.fill(2, 16, 147);\n        // p.fill(19, 50, 18);\n        // p.fill(178, 0, 36);\n\n        quadtree = new QuadTree({ x: 0, y: 0, w: p.width, h: p.height }, 4);\n        for (let particle of particles) {\n          quadtree.insert(particle);\n        }\n\n        const mouseVelocityX = p.mouseX - prevMouseX;\n        const mouseVelocityY = p.mouseY - prevMouseY;\n\n        const range = { x: p.mouseX, y: p.mouseY, r: influenceRadius };\n        const found: Particle[] = [];\n        quadtree.query(range, found);\n\n        for (let particle of found) {\n          const dx = p.mouseX - particle.x;\n          const dy = p.mouseY - particle.y;\n          const distance = p.sqrt(dx * dx + dy * dy);\n\n          if (distance < influenceRadius) {\n            const distanceInfluence = p.map(\n              distance,\n              0,\n              influenceRadius,\n              1,\n              0,\n              true\n            );\n            const force = maxSpeed * distanceInfluence * 0.1;\n\n            particle.vx = mouseVelocityX * force;\n            particle.vy = mouseVelocityY * force;\n          }\n        }\n\n        for (let particle of particles) {\n          particle.x += particle.vx;\n          particle.y += particle.vy;\n\n          // Immediate return to original position\n          particle.x = p.lerp(particle.x, particle.originalX, 0.1);\n          particle.y = p.lerp(particle.y, particle.originalY, 0.1);\n\n          p.text(density, particle.x, particle.y);\n        }\n\n        if (!signaledReady) {\n          signaledReady = true;\n          if (containerEl) containerEl.style.visibility = \"visible\";\n          if (!readyRef.current) {\n            readyRef.current = true;\n            setReady(true);\n          }\n        }\n        prevMouseX = p.mouseX;\n        prevMouseY = p.mouseY;\n      };\n    };\n\n    const p5Instance = new p5(sketch, sketchRef.current);\n\n    return () => {\n      p5Instance.remove();\n    };\n  }, []);\n\n  return (\n    <motion.div\n      className=\"flex items-center justify-center overflow-hidden\"\n      initial={false}\n      animate={{ opacity: ready ? 1 : 0 }}\n      exit={{ opacity: 0 }}\n      transition={{ duration: 0.6, ease: \"easeOut\" }}\n      style={{ opacity: 0, willChange: \"opacity\" }}\n    >\n      <div ref={sketchRef} style={{ opacity: 0.65, visibility: \"hidden\" }} />\n    </motion.div>\n  );\n};\n\nexport default P5AsciiTree;\n"
  },
  {
    "path": "components/twitter-x-loop.tsx",
    "content": "\"use client\";\n\nimport { useState, useRef, useCallback } from \"react\";\nimport { motion, AnimatePresence, type Variants } from \"motion/react\";\nimport Link from \"next/link\";\n\nconst motionVariants: Variants = {\n  initial: { y: 10, opacity: 0, filter: \"blur(2px)\" },\n  animate: { y: 0, opacity: 1, filter: \"blur(0px)\" },\n  exit: { y: -10, opacity: 0, filter: \"blur(2px)\" },\n};\n\nexport default function TwitterXMotion({ className }: { className: string }) {\n  const [isX, setIsX] = useState(false);\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n  const handleMouseEnter = useCallback(() => {\n    timeoutRef.current = setTimeout(() => {\n      setIsX(true);\n    }, 400);\n  }, []);\n\n  const handleMouseLeave = useCallback(() => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n    }\n    setIsX(false);\n  }, []);\n\n  return (\n    <div\n      className={className}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.div\n          className=\"w-12 font-medium\"\n          key={isX ? \"X\" : \"Twitter\"}\n          variants={motionVariants}\n          initial=\"initial\"\n          animate=\"animate\"\n          exit=\"exit\"\n          transition={{ duration: 0.1 }}\n        >\n          {isX ? \"X 🫨\" : \"Twitter\"}\n        </motion.div>\n      </AnimatePresence>\n      <Link\n        href=\"https://twitter.com/typicalmitul\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"hover:underline underline-offset-2\"\n      >\n        @typicalmitul\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/video-hover-preview.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport LinkPrimitive, { link } from \"./link-primitive\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport * as HoverCard from \"@radix-ui/react-hover-card\";\nimport { track } from \"@vercel/analytics/react\";\n\nconst extractYouTubeId = (url: string): string | null => {\n  const patterns = [\n    /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|youtube\\.com\\/embed\\/)([^&\\n?#]+)/,\n    /youtube\\.com\\/v\\/([^&\\n?#]+)/,\n  ];\n\n  for (const pattern of patterns) {\n    const match = url.match(pattern);\n    if (match) return match[1];\n  }\n  return null;\n};\n\nconst VideoHoverPreview = ({\n  href,\n  children,\n}: {\n  href: string;\n  children: React.ReactNode;\n}) => {\n  const [imageLoaded, setImageLoaded] = useState(true);\n\n  const videoId = extractYouTubeId(href);\n  const thumbnailUrl = videoId\n    ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`\n    : null;\n\n  return (\n    <HoverCard.Root openDelay={75} closeDelay={150}>\n      <HoverCard.Trigger asChild>\n        <LinkPrimitive\n          className={cn(link({ variant: \"default\", popover: false }))}\n          external\n          href={href}\n          onClick={(e) => {\n            e.stopPropagation();\n            track(\"video_hover_preview_clicked\");\n          }}\n        >\n          {children}\n        </LinkPrimitive>\n      </HoverCard.Trigger>\n\n      <HoverCard.Portal>\n        <HoverCard.Content\n          className=\"rounded-[2px] border border-accent overflow-hidden w-30 z-50 transform-gpu origin-center transition-all \n          data-[side=bottom]:animate-blur-and-slide-up data-[side=top]:animate-blur-and-slide-down data-[state=open]:transition-all hover:cursor-pointer hover:-rotate-3\"\n          sideOffset={8}\n          align=\"center\"\n          side=\"top\"\n        >\n          <Link\n            href={href}\n            className=\"block relative aspect-video rounded-[2px]\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            onClick={(e) => {\n              e.stopPropagation();\n              track(\"video_hover_preview_clicked\");\n            }}\n          >\n            {thumbnailUrl ? (\n              <Image\n                src={thumbnailUrl}\n                alt={\"Thumbnail for Casey Neistat's Doing What I Can't\"}\n                fill\n                className={cn(\"w-full h-full object-cover\")}\n                quality={40}\n              />\n            ) : (\n              <div className=\"w-full h-full bg-gray-3 animate-pulse flex items-center justify-center\">\n                <div className=\"text-gray-9 text-sm\">Loading thumbnail...</div>\n              </div>\n            )}\n          </Link>\n        </HoverCard.Content>\n      </HoverCard.Portal>\n    </HoverCard.Root>\n  );\n};\n\nexport default VideoHoverPreview;\n"
  },
  {
    "path": "components/video-pause-button.tsx",
    "content": "\"use client\";\n\nimport { Pause, Play } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\n\nexport default function VideoPauseButton() {\n  const [isVideoPaused, setIsVideoPaused] = useState(false);\n\n  const handleToggleVideo = () => {\n    setIsVideoPaused(!isVideoPaused);\n    const video = document.querySelector(\n      'video[src*=\"shader-vid.mp4\"]'\n    ) as HTMLVideoElement;\n    if (video) {\n      if (video.paused) {\n        video.play();\n      } else {\n        video.pause();\n      }\n    }\n  };\n\n  return (\n    <button\n      onClick={handleToggleVideo}\n      className=\"flex gap-x-1.5 items-center bg-accent hover:bg-accent/80 transition text-gray-1 py-0.5 pl-1.5 pr-1.5 rounded-[2px] cursor-pointer font-medium\"\n      aria-label=\"Toggle background video playback\"\n    >\n      {isVideoPaused ? (\n        <Play weight=\"fill\" size={12} aria-hidden={true} />\n      ) : (\n        <Pause weight=\"fill\" size={12} aria-hidden={true} />\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "components/visitors/approve-btn.tsx",
    "content": "\"use client\";\nimport { approveGuestbookEntry, declineGuestbookEntry } from \"@/app/actions\";\nimport { localEntriesAtom } from \"@/atoms/guestbook\";\nimport { useSetAtom } from \"jotai\";\nimport { useActionState } from \"react\";\nimport { Spinner } from \"@phosphor-icons/react\";\n\nconst ApproveButton = ({ id }: { id: string }) => {\n  const setLocalEntries = useSetAtom(localEntriesAtom);\n  \n  const [approveState, approveAction, approvePending] = useActionState(\n    async () => {\n      await approveGuestbookEntry(id);\n      setLocalEntries((prev) => prev.filter((entry) => entry.id !== id));\n    },\n    null\n  );\n\n  const [declineState, declineAction, declinePending] = useActionState(\n    async () => {\n      await declineGuestbookEntry(id);\n      setLocalEntries((prev) => prev.filter((entry) => entry.id !== id));\n    },\n    null\n  );\n\n  return (\n    <div className=\"flex gap-x-2\">\n      <form action={approveAction}>\n        <button\n          className=\"bg-[#267E5C] rounded-[6px] px-2 py-1 text-gray-1 font-medium mt-2 hover:bg-[#267E5C]/80 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1\"\n          type=\"submit\"\n          disabled={approvePending || declinePending}\n        >\n          {approvePending && <Spinner className=\"animate-spin\" size={16} />}\n          Approve\n        </button>\n      </form>\n      <form action={declineAction}>\n        <button\n          className=\"bg-[#F74F00] rounded-[6px] px-2 py-1 text-gray-1 font-medium mt-2 hover:bg-[#F74F00]/80 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1\"\n          type=\"submit\"\n          disabled={approvePending || declinePending}\n        >\n          {declinePending && <Spinner className=\"animate-spin\" size={16} />}\n          Decline\n        </button>\n      </form>\n    </div>\n  );\n};\n\nexport default ApproveButton;\n"
  },
  {
    "path": "components/visitors/cta.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useFormStatus } from \"react-dom\";\nimport useMeasure from \"react-use-measure\";\nimport { AnimatePresence, motion, MotionConfig } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport useClickOutside from \"@/hooks/useClickOutside\";\nimport Signature, { type SignatureRef } from \"@uiw/react-signature\";\nimport styles from \"./visitors.module.css\";\nimport { ArrowClockwise } from \"@phosphor-icons/react\";\nimport { validateAndSaveEntry } from \"@/app/(without-root-layout)/visitors/actions\";\nimport Field from \"./field\";\nimport { useAtom, useSetAtom } from \"jotai\";\nimport {\n  hasCreatedEntryBeforeAtom,\n  localCreatedByIdAtom,\n  localEntriesAtom,\n} from \"@/atoms/guestbook\";\n\nconst transition = {\n  type: \"spring\",\n  bounce: 0.1,\n  duration: 0.25,\n} as const;\n\nexport default function WriteNoteCTA() {\n  const [step, setStep] = useState<number>(0);\n  const [contentRef, { height: heightContent }] = useMeasure();\n  const [menuRef, { width: widthContainer }] = useMeasure();\n  const [maxWidth, setMaxWidth] = useState(0);\n  const [formInfo, setFormInfo] = useState({\n    created_by: \"\",\n    entry: \"\",\n    signature: \"\",\n  });\n  const [isOpen, setIsOpen] = useState(false);\n  const [errors, setErrors] = useState<Record<string, string[]> | null>(null);\n\n  const setLocalEntries = useSetAtom(localEntriesAtom);\n  const [hasCreatedEntryBefore, setHasCreatedEntryBefore] = useAtom(\n    hasCreatedEntryBeforeAtom\n  );\n  const [localCreatedById, setLocalCreatedById] = useAtom(localCreatedByIdAtom);\n\n  const buttonText = [\"Write me a note\", \"Next\", \"Submit\", \"Thanks!\"][step];\n\n  const ref = useRef<HTMLDivElement>(null);\n  const formRef = useRef<HTMLFormElement>(null);\n  const $svg = useRef<SignatureRef>(null);\n  const { pending } = useFormStatus();\n  const [loading, setLoading] = useState(false);\n\n  const handleCreatedByChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormInfo({\n      ...formInfo,\n      created_by: e.target.value,\n    });\n  };\n\n  const handleEntryChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormInfo({\n      ...formInfo,\n      entry: e.target.value,\n    });\n  };\n\n  const handleSVGCapture = () => {\n    const svgelm = $svg.current?.svg?.cloneNode(true) as SVGSVGElement;\n    const clientWidth = $svg.current?.svg?.clientWidth;\n    const clientHeight = $svg.current?.svg?.clientHeight;\n    svgelm.removeAttribute(\"style\");\n    svgelm.setAttribute(\"width\", `${clientWidth}px`);\n    svgelm.setAttribute(\"height\", `${clientHeight}px`);\n    svgelm.setAttribute(\"viewbox\", `${clientWidth} ${clientHeight}`);\n    setFormInfo((prev) => ({\n      ...prev,\n      signature: svgelm.outerHTML,\n    }));\n    return svgelm.outerHTML;\n  };\n\n  const stepConent = (\n    step: number,\n    svgRef: React.RefObject<SignatureRef | null>\n  ) => {\n    switch (step) {\n      case 1:\n        return (\n          <fieldset className=\"flex flex-col gap-y-4 p-2\">\n            <Field\n              label=\"ur name, handle, something\"\n              value={formInfo.created_by}\n              name=\"created_by\"\n              placeholder=\"uncle ben\"\n              onChange={handleCreatedByChange}\n            />\n            <Field\n              label=\"a sweet likkle note\"\n              value={formInfo.entry}\n              name=\"entry\"\n              placeholder=\"with great power...\"\n              onChange={handleEntryChange}\n            />\n          </fieldset>\n        );\n      case 2:\n        return (\n          <div className=\"rounded-6 overflow-hidden bg-gray-1 p-0.5 flex flex-col relative h-36\">\n            <Signature\n              ref={svgRef}\n              options={{\n                size: 10,\n                thinning: 0.25,\n              }}\n            />\n            <input type=\"hidden\" value={formInfo.signature} />\n            <button\n              aria-label=\"clear signature\"\n              className=\"rounded-[4px] text-gray-11 font-medium self-end absolute bottom-1 left-1 bg-gray-6 p-1 group hover:bg-gray-8 hover:text-gray-12 transition duration-200\"\n              type=\"button\"\n              onClick={() => svgRef.current?.clear()}\n            >\n              <ArrowClockwise className=\"group-hover:rotate-180 transition duration-200 \" />\n            </button>\n          </div>\n        );\n      default:\n        return null;\n    }\n  };\n\n  const validateStep = async (currentStep: number) => {\n    setLoading(true);\n    if (currentStep === 1) {\n      const formData = new FormData();\n      formData.append(\"created_by\", formInfo.created_by);\n      formData.append(\"entry\", formInfo.entry);\n\n      const result = await validateAndSaveEntry(formData, true);\n\n      if (!result.success) {\n        //@ts-ignore\n        setErrors(result.errors);\n        setLoading(false);\n        return false;\n      }\n      setErrors(null);\n      setLoading(false);\n      return true;\n    }\n    setLoading(false);\n    return true;\n  };\n\n  const handleClick = async () => {\n    if (!localCreatedById) setLocalCreatedById(crypto.randomUUID());\n    if (step === 3) {\n      return;\n    }\n\n    if (!isOpen && step === 0) {\n      setIsOpen(true);\n      setStep(1);\n      return;\n    }\n\n    if (!isOpen) {\n      setIsOpen(true);\n      return;\n    }\n\n    if (step === 1) {\n      const isValid = await validateStep(step);\n      if (!isValid) return;\n    }\n\n    if (step === 2) {\n      setLoading(true);\n      const s = handleSVGCapture();\n      if (!s) {\n        setLoading(false);\n        return;\n      }\n\n      const formData = new FormData();\n      formData.append(\"local_entry_id\", crypto.randomUUID());\n      formData.append(\"created_by\", formInfo.created_by);\n      formData.append(\"entry\", formInfo.entry);\n      formData.append(\"signature\", s);\n      formData.append(\n        \"hasCreatedEntryBefore\",\n        hasCreatedEntryBefore.toString()\n      );\n      formData.append(\"local_created_by_id\", localCreatedById);\n      await handleSubmit(formData);\n      return;\n    }\n\n    setStep((prev) => prev + 1);\n  };\n\n  const getRandomPosition = (min: number, max: number) =>\n    Math.random() * (max - min) + min;\n\n  const handleSubmit = async (formData: FormData) => {\n    const result = await validateAndSaveEntry(formData);\n    if (!result.success) {\n      //@ts-ignore\n      setErrors(result.errors);\n      setLoading(false);\n      return;\n    }\n\n    const newEntry = {\n      id: crypto.randomUUID(),\n      local_entry_id: formData.get(\"local_entry_id\") as string,\n      created_by: formData.get(\"created_by\") as string,\n      body: formData.get(\"entry\") as string,\n      signature: formData.get(\"signature\") as string,\n      initialX: getRandomPosition(100, window.innerWidth - 100),\n      initialY: getRandomPosition(100, window.innerHeight - 100),\n    };\n    setLocalEntries((prev) => [newEntry, ...prev]);\n\n    setStep(3);\n    setIsOpen(false);\n    setLoading(false);\n    formRef.current?.reset();\n    setHasCreatedEntryBefore(true);\n  };\n\n  useClickOutside(ref, () => {\n    setIsOpen(false);\n  });\n\n  useEffect(() => {\n    if (!widthContainer || maxWidth > 0) return;\n    setMaxWidth(widthContainer);\n  }, [widthContainer, maxWidth]);\n\n  return (\n    <div className=\"bottom-10 left-1/2 -translate-x-1/2 absolute z-300\">\n      <div\n        className={cn(\n          \"rounded-6 bg-[#F04F1F] transition text-[1.5rem] flex gap-x-1.5 items-center justify-center text-gray-1 font-semibold h-fit w-72\",\n          styles.homeBtn\n        )}\n      >\n        <MotionConfig transition={transition}>\n          <div className=\"h-full w-full\" ref={ref}>\n            <form ref={formRef}>\n              <div className=\"overflow-hidden w-full\">\n                <AnimatePresence initial={false} mode=\"sync\">\n                  {isOpen ? (\n                    <motion.div\n                      key=\"content\"\n                      initial={{ height: 0 }}\n                      animate={{ height: heightContent || 0 }}\n                      exit={{ height: 0 }}\n                      style={{\n                        width: maxWidth,\n                      }}\n                    >\n                      <div ref={contentRef} className=\"w-full\">\n                        <motion.div\n                          key={\"notes\"}\n                          initial={{ opacity: 0 }}\n                          animate={{ opacity: isOpen ? 1 : 0 }}\n                          exit={{ opacity: 0 }}\n                        >\n                          <div\n                            className={cn(\n                              \"px-2 pt-2 text-sm\",\n                              isOpen ? \"block\" : \"hidden\"\n                            )}\n                          >\n                            <AnimatePresence>\n                              {step === 1 && (\n                                <motion.div\n                                  className={cn(\n                                    \"absolute -top-[4.5rem] w-full left-0 bg-[#101B1D] text-[1rem] rounded-6 shadow-lg px-4 py-2 font-medium text-center transition\",\n                                    errors\n                                      ? \"ring-2 ring-[red]/60\"\n                                      : \"text-gray-1\"\n                                  )}\n                                  style={{\n                                    textWrap: \"balance\",\n                                  }}\n                                  initial={{ opacity: 0, y: -20 }}\n                                  animate={{ opacity: 1, y: 0 }}\n                                  exit={{\n                                    opacity: 0,\n                                    y: -20,\n                                    transition: {\n                                      duration: 0.1,\n                                    },\n                                  }}\n                                  transition={{\n                                    type: \"spring\",\n                                    duration: 0.05,\n                                    bounce: 0.02,\n                                    restDelta: 0.01,\n                                  }}\n                                >\n                                  <AnimatePresence mode=\"wait\" initial={false}>\n                                    <motion.p\n                                      key={\n                                        errors?.created_by || errors?.entry\n                                          ? \"error\"\n                                          : \"default\"\n                                      }\n                                      initial={{ opacity: 0, y: -10 }}\n                                      animate={{ opacity: 1, y: 0 }}\n                                      exit={{ opacity: 0, y: 10 }}\n                                      transition={{ duration: 0.05 }}\n                                    >\n                                      {errors?.created_by || errors?.entry\n                                        ? errors?.created_by || errors?.entry\n                                        : `tnx for visiting! leave ur name and a note if u\n                                want... <3`}\n                                    </motion.p>\n                                  </AnimatePresence>\n                                </motion.div>\n                              )}\n                              {step === 2 && (\n                                <motion.div\n                                  className=\"absolute -top-[4.5rem] w-full left-0 bg-[#101B1D] text-[1rem] rounded-6 shadow-lg px-4 py-2 font-medium text-center transition\"\n                                  style={{\n                                    textWrap: \"balance\",\n                                  }}\n                                  initial={{ opacity: 0, y: -20 }}\n                                  animate={{ opacity: 1, y: 0 }}\n                                  exit={{\n                                    opacity: 0,\n                                    y: -20,\n                                    transition: {\n                                      duration: 0.1,\n                                    },\n                                  }}\n                                >\n                                  why not a little drawing as well!{\" \"}\n                                  <span>\n                                    be creative!! no more smiley faces\n                                  </span>\n                                </motion.div>\n                              )}\n                            </AnimatePresence>\n                            {stepConent(step, $svg)}\n                          </div>\n                        </motion.div>\n                      </div>\n                    </motion.div>\n                  ) : null}\n                </AnimatePresence>\n              </div>\n\n              <button\n                ref={menuRef}\n                aria-label={\"notes\"}\n                className={cn(\n                  \"relative flex py-4 w-full shrink-0 scale-100 select-none appearance-none items-center justify-center transition focus-visible:ring-2 active:scale-[0.98] lowercase\",\n                  loading ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\"\n                )}\n                type=\"button\"\n                disabled={pending || loading}\n                onClick={handleClick}\n              >\n                {isOpen || step === 3 ? buttonText : \"write me a note\"}\n              </button>\n            </form>\n          </div>\n        </MotionConfig>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/visitors/drag.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport useMaxZIndex from \"@/hooks/useMaxZIndex\";\nimport { cn, getRandomRotation } from \"@/lib/utils\";\nimport { motion, type PanInfo, useAnimation } from \"motion/react\";\nimport React from \"react\";\n\nconst Drag = React.memo(\n  ({\n    children,\n    className,\n    initialX,\n    initialY,\n    ...props\n  }: {\n    children: React.ReactNode;\n    className?: string;\n    initialX?: number;\n    initialY?: number;\n  }) => {\n    const [zIndex, updateZIndex] = useMaxZIndex();\n    const controls = useAnimation();\n    const r = getRandomRotation();\n    const [initialRotate] = useState(r);\n    const [x, y] = [\n      initialX ?? Math.floor(Math.random() * 1300),\n      initialY ?? Math.floor(Math.random() * 900),\n    ];\n\n    const handleDragEnd = (event: MouseEvent, info: PanInfo) => {\n      const direction = info.offset.x > 0 ? 1 : -1;\n      const velocity = Math.min(Math.abs(info.velocity.x), 1);\n      controls.start({\n        rotate: Math.floor(initialRotate + velocity * 20 * direction),\n        transition: {\n          type: \"spring\",\n          duration: 1,\n          stiffness: 50,\n          damping: 30,\n          mass: 1,\n          restDelta: 0.001,\n        },\n      });\n    };\n\n    return (\n      <motion.div\n        drag\n        dragElastic={0.2}\n        className={cn(\n          \"select-none w-fit h-fit drag-elements absolute\",\n          className\n        )}\n        dragTransition={{ power: 0.2, timeConstant: 200 }}\n        onMouseDown={updateZIndex}\n        onTouchStart={updateZIndex}\n        onDragEnd={handleDragEnd}\n        animate={controls}\n        initial={{\n          rotate: r,\n          x,\n          y,\n        }}\n        style={{\n          zIndex,\n        }}\n        {...props}\n      >\n        {children}\n      </motion.div>\n    );\n  }\n);\n\nexport default Drag;\n"
  },
  {
    "path": "components/visitors/field.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useId } from \"react\";\nimport styles from \"./visitors.module.css\";\n\ninterface FieldProps {\n  value: string;\n  label: string;\n  name: string;\n  placeholder?: string;\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n}\n\nconst Field = ({\n  value,\n  label,\n  onChange,\n  name,\n  placeholder,\n  ...props\n}: FieldProps) => {\n  const id = useId();\n\n  return (\n    <div className=\"flex flex-col gap-y-1\">\n      <label className=\"font-medium text-[14px]\" htmlFor={id}>\n        {label}\n      </label>\n      <input\n        type=\"text\"\n        name={name}\n        id={id}\n        placeholder={placeholder}\n        required\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        className={cn(\n          \"bg-[#101B1D]/30 focus:bg-gray-1 transition-all focus:placeholder:text-gray-9 text-[16px] outline-hidden text-gray-2 focus:text-gray-12 font-normal rounded-[6px] p-3 w-full placeholder:text-white/40 \",\n          styles.input\n        )}\n        onChange={onChange}\n        value={value}\n        {...props}\n      />\n    </div>\n  );\n};\n\nexport default Field;\n"
  },
  {
    "path": "components/visitors/guestbook-entries.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useAtom } from \"jotai\";\nimport Note from \"@/components/visitors/note\";\nimport {\n  allEntriesAtom,\n  localEntriesAtom,\n  serverEntriesAtom,\n} from \"@/atoms/guestbook\";\nimport { getGuestbookEntries } from \"@/app/(without-root-layout)/visitors/actions\";\n\nfunction GuestbookEntries() {\n  const [allEntries] = useAtom(allEntriesAtom);\n  const [, setServerEntries] = useAtom(serverEntriesAtom);\n  const [, setLocalEntries] = useAtom(localEntriesAtom);\n\n  useEffect(() => {\n    const fetchEntries = async () => {\n      const entries = await getGuestbookEntries();\n      // @ts-ignore\n      setServerEntries(entries);\n\n      // if entries contains an approved entry that matches an entry in localEntries, remove it\n      setLocalEntries((prev) =>\n        prev.filter(\n          (localEntry) =>\n            !entries.some(\n              (entry) =>\n                (entry.local_entry_id === localEntry.local_entry_id &&\n                  entry.approved) ||\n                (entry.signature === localEntry.signature && entry.approved)\n            )\n        )\n      );\n    };\n\n    fetchEntries();\n  }, [setServerEntries, setLocalEntries]);\n\n  return allEntries.map((entry) => (\n    <Note\n      key={entry.id}\n      name={entry.created_by}\n      content={entry.body}\n      signature={entry.signature}\n      initialX={entry.initialX}\n      initialY={entry.initialY}\n    />\n  ));\n}\n\nexport default GuestbookEntries;\n"
  },
  {
    "path": "components/visitors/note.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport styles from \"./visitors.module.css\";\nimport Image from \"next/image\";\nimport Drag from \"./drag\";\nimport React from \"react\";\nimport { motion } from \"motion/react\";\n\nconst Note = React.memo(\n  ({\n    name,\n    content,\n    signature,\n    initialX,\n    initialY,\n  }: {\n    name: string;\n    content: string;\n    signature: string;\n    initialX?: number;\n    initialY?: number;\n  }) => {\n    return (\n      <Drag\n        className={cn(\"z-10 max-w-[200px]\")}\n        initialX={initialX}\n        initialY={initialY}\n      >\n        <motion.div\n          initial={{ opacity: 0, y: 2 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.3, ease: \"easeOut\" }}\n          className={cn(\n            \"bg-gray-1 text-gray-12 w-fit max-w-[165px] scale-75! px-1.5 pt-1.5 pb-2 transition-shadow duration-300 ease-out hover:shadow-md note-item\",\n            styles.note\n          )}\n        >\n          {signature ? (\n            <div\n              className={cn(\n                \"border border-gray-6 bg-gray-3 rounded-[4px] flex items-center justify-center overflow-hidden relative\"\n              )}\n            >\n              <div\n                className=\"object-contain z-10\"\n                // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n                dangerouslySetInnerHTML={{ __html: signature }}\n              />\n              <Image\n                src=\"/images/33.jpeg\"\n                className=\"absolute object-cover\"\n                fill\n                draggable={false}\n                quality={25}\n                alt=\"\"\n              />\n            </div>\n          ) : null}\n          <div className=\"w-full text-sm break-words mt-1.5\">\n            <span className=\"text-gray-11 text-[14px] mr-1 font-semibold\">\n              {name}\n            </span>\n            <div className=\"text-[16px] font-medium leading-tight\">\n              {content}\n            </div>\n          </div>\n        </motion.div>\n      </Drag>\n    );\n  }\n);\n\nexport default Note;\n"
  },
  {
    "path": "components/visitors/polaroid.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport styles from \"./visitors.module.css\";\nimport React from \"react\";\nimport Drag from \"./drag\";\nimport Image from \"next/image\";\n\nconst Polaroid = ({ src, alt }: { src: string; alt: string }) => {\n  return (\n    <Drag\n      className={cn(\n        \"p-1 pb-6 bg-gray-1 rounded-[8px] transition-all duration-300 ease-out hover:shadow-md\",\n        styles.polaroid\n      )}\n    >\n      <div className=\"h-full overflow-hidden relative\">\n        <Image\n          src={src}\n          alt={alt}\n          fill\n          sizes=\"(max-width: 768px) 20vw, 35vw\"\n          className=\"max-w-full h-fit object-contain\"\n          draggable={false}\n          quality={5}\n        />\n      </div>\n    </Drag>\n  );\n};\n\nexport default Polaroid;\n"
  },
  {
    "path": "components/visitors/stickers.tsx",
    "content": "\"use client\";\n\nimport Drag from \"./drag\";\n\nconst NextWordmark = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 394 79\"\n      className=\"w-24 bg-gray-1 rounded-full p-2 border border-gray-6 overflow-visible\"\n    >\n      <title>Next.js Logo</title>\n      <path d=\"M261.919 0.0330722H330.547V12.7H303.323V79.339H289.71V12.7H261.919V0.0330722Z\" />\n      <path d=\"M149.052 0.0330722V12.7H94.0421V33.0772H138.281V45.7441H94.0421V66.6721H149.052V79.339H80.43V12.7H80.4243V0.0330722H149.052Z\" />\n      <path d=\"M183.32 0.0661486H165.506L229.312 79.3721H247.178L215.271 39.7464L247.127 0.126654L229.312 0.154184L206.352 28.6697L183.32 0.0661486Z\" />\n      <path d=\"M201.6 56.7148L192.679 45.6229L165.455 79.4326H183.32L201.6 56.7148Z\" />\n      <path\n        clipRule=\"evenodd\"\n        d=\"M80.907 79.339L17.0151 0H0V79.3059H13.6121V16.9516L63.8067 79.339H80.907Z\"\n        fillRule=\"evenodd\"\n      />\n      <path d=\"M333.607 78.8546C332.61 78.8546 331.762 78.5093 331.052 77.8186C330.342 77.1279 329.991 76.2917 330 75.3011C329.991 74.3377 330.342 73.5106 331.052 72.8199C331.762 72.1292 332.61 71.7838 333.607 71.7838C334.566 71.7838 335.405 72.1292 336.115 72.8199C336.835 73.5106 337.194 74.3377 337.204 75.3011C337.194 75.9554 337.028 76.5552 336.696 77.0914C336.355 77.6368 335.922 78.064 335.377 78.373C334.842 78.6911 334.252 78.8546 333.607 78.8546Z\" />\n      <path d=\"M356.84 45.4453H362.872V68.6846C362.863 70.8204 362.401 72.6472 361.498 74.1832C360.585 75.7191 359.321 76.8914 357.698 77.7185C356.084 78.5364 354.193 78.9546 352.044 78.9546C350.079 78.9546 348.318 78.6001 346.75 77.9094C345.182 77.2187 343.937 76.1826 343.024 74.8193C342.101 73.456 341.649 71.7565 341.649 69.7207H347.691C347.7 70.6114 347.903 71.3838 348.29 72.0291C348.677 72.6744 349.212 73.1651 349.895 73.5105C350.586 73.8559 351.38 74.0286 352.274 74.0286C353.243 74.0286 354.073 73.8286 354.746 73.4196C355.419 73.0197 355.936 72.4199 356.296 71.6201C356.646 70.8295 356.831 69.8479 356.84 68.6846V45.4453Z\" />\n      <path d=\"M387.691 54.5338C387.544 53.1251 386.898 52.0254 385.773 51.2438C384.638 50.4531 383.172 50.0623 381.373 50.0623C380.11 50.0623 379.022 50.2532 378.118 50.6258C377.214 51.0075 376.513 51.5164 376.033 52.1617C375.554 52.807 375.314 53.5432 375.295 54.3703C375.295 55.061 375.461 55.6608 375.784 56.1607C376.107 56.6696 376.54 57.0968 377.103 57.4422C377.656 57.7966 378.274 58.0874 378.948 58.3237C379.63 58.56 380.313 58.76 380.995 58.9236L384.14 59.6961C385.404 59.9869 386.631 60.3778 387.802 60.8776C388.973 61.3684 390.034 61.9955 390.965 62.7498C391.897 63.5042 392.635 64.413 393.179 65.4764C393.723 66.5397 394 67.7848 394 69.2208C394 71.1566 393.502 72.8562 392.496 74.3285C391.491 75.7917 390.043 76.9369 388.143 77.764C386.252 78.582 383.965 79 381.272 79C378.671 79 376.402 78.6002 374.493 77.8004C372.575 77.0097 371.08 75.8463 370.001 74.3194C368.922 72.7926 368.341 70.9294 368.258 68.7391H374.235C374.318 69.8842 374.687 70.8386 375.314 71.6111C375.95 72.3745 376.78 72.938 377.795 73.3197C378.819 73.6923 379.962 73.8832 381.226 73.8832C382.545 73.8832 383.707 73.6832 384.712 73.2924C385.708 72.9016 386.492 72.3564 387.055 71.6475C387.627 70.9476 387.913 70.1206 387.922 69.1754C387.913 68.312 387.654 67.5939 387.156 67.0304C386.649 66.467 385.948 65.9944 385.053 65.6127C384.15 65.231 383.098 64.8856 381.899 64.5857L378.081 63.6223C375.323 62.9225 373.137 61.8592 371.541 60.4323C369.937 59.0054 369.143 57.115 369.143 54.7429C369.143 52.798 369.678 51.0894 370.758 49.6261C371.827 48.1629 373.294 47.0268 375.148 46.2179C377.011 45.4 379.114 45 381.456 45C383.836 45 385.92 45.4 387.719 46.2179C389.517 47.0268 390.929 48.1538 391.952 49.5897C392.976 51.0257 393.511 52.6707 393.539 54.5338H387.691Z\" />\n    </svg>\n  );\n};\n\nconst VercelLogo = ({ ...props }) => {\n  return (\n    <svg\n      width=\"76\"\n      height=\"65\"\n      viewBox=\"0 0 76 65\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      style={{\n        shapeRendering: \"crispEdges\",\n        filter: \"drop-shadow(0 2px 1px rgba(0,0,0,.1))\",\n        overflow: \"visible\",\n      }}\n      aria-label=\"Vercel logo as sticker\"\n      {...props}\n    >\n      <title>Vercel Logo</title>\n      <path\n        d=\"M37.5274 0L75.0548 65H0L37.5274 0Z\"\n        fill=\"#000\"\n        stroke=\"#fff\"\n        strokeWidth=\"4\"\n        strokeLinejoin=\"round\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n};\n\nconst Sticker = ({ children }: { children: React.ReactNode }) => {\n  return <Drag className=\"drop-shadow-xs\">{children}</Drag>;\n};\n\nexport { VercelLogo, Sticker, NextWordmark };\n"
  },
  {
    "path": "components/visitors/visitors.module.css",
    "content": ".note {\n  z-index: 10;\n  border: 1px solid rgba(0, 0, 0, 0.1);\n  /* box-shadow: 0.3400000035762787px 0.3400000035762787px 0.3400000035762787px\n      #1d2d5233,\n    0 0 0.3400000035762787px 0.3400000035762787px #1e2d520f; */\n  border-radius: 8px;\n  backdrop-blur: 6px;\n  /* background-color: #0a4a31; */\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.2),\n    0 16px 32px rgba(0, 0, 0, 0.2);\n}\n\n.polaroid {\n  width: 125px;\n  height: 160px;\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 8px 16px rgba(0, 0, 0, 0.2),\n    0 16px 32px rgba(0, 0, 0, 0.2);\n}\n\n.polaroid img {\n  width: 100%;\n  height: 100%;\n  border-radius: 4px;\n  object-fit: cover;\n  object-position: top;\n}\n\n.input {\n  box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset;\n}\n\n.homeBtn {\n  box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px,\n    rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset,\n    inset 0 1px 0 0 #ffffff52;\n}\n"
  },
  {
    "path": "content.ts",
    "content": "export const experiences = [\n  {\n    company: \"Vercel\",\n    role: \"Design Engineer\",\n    link: \"https://vercel.com/?ref=mitulca\",\n    range: \"Now\",\n    description: \"Building for the future of the web while doing my best work\",\n  },\n  {\n    company: \"Compound\",\n    role: \"Software Engineer\",\n    link: \"https://compoundplanning.com/?ref=mitulca\",\n    range: \"2022 - 2023\",\n    description:\n      \"Built charting components, rebuilt the app navigation and worked on improving UX for advisors to thrive in supporting clients\",\n    skills: [\"React\", \"TypeScript\", \"Redux\", \"CSS-in-JS\", \"Next.js\"],\n  },\n  {\n    company: \"Composer\",\n    link: \"https://composer.trade/?ref=mitulca\",\n    role: \"Design Engineer\",\n    range: \"2021 - 2022\",\n    description:\n      \"As an early employee, I built out over 90% of the application UI and a scalable and accessible component library\",\n    skills: [\"React\", \"ClojureScript\", \"TailwindCSS\", \"Contentful\", \"Next.js\"],\n  },\n  // {\n  //   company: \"Hypercontext\",\n  //   role: \"Product Analyst\",\n  //   range: \"2019 - 2020\",\n  //   description:\n  //     \"Designed growth experiments to convert users from free to paid, built the sales operations and email-marketing playbook from the ground up\",\n  // },\n  {\n    company: \"Uber\",\n    role: \"Operations Intern\",\n    range: \"2018\",\n    description:\n      \"Led competitive research for Canada and supported the launch of 30 cities in 1 day through building courier acquisition campaigns\",\n  },\n];\n\nexport const Status = {\n  none: \"none\",\n  progress: \"progress\",\n  completed: \"completed\",\n} as const;\n\nexport const photos = [\n  {\n    src: \"/images/daniel.jpg\",\n    alt: \"R&B artist Daniel Caesar performing at the Scotiabank Arena in Toronto, Canada\",\n  },\n  {\n    src: \"/images/maggie.jpg\",\n    alt: \"Indie artist Maggie Rogers performing at the Budweiser Stage in Toronto, Canada\",\n  },\n  {\n    src: \"/images/toronto.jpg\",\n    alt: \"A photo of the CN Tower in Toronto above the clouds with blue skies behind it\",\n  },\n  {\n    src: \"/images/nyc.jpg\",\n    alt: \"A classic photo of the New York City skyline taken at dusk from the Top of the Rock\",\n  },\n  {\n    src: \"/images/banff-2.jpg\",\n    alt: \"\",\n  },\n  {\n    src: \"/images/banff.jpg\",\n    alt: \"\",\n  },\n];\n\nexport const bucketList = [\n  {\n    item: \"Travel the world\",\n    status: Status.completed,\n  },\n  {\n    item: \"Visit Iceland\",\n    status: Status.none,\n  },\n  {\n    item: \"Do a backflip in every contintent\",\n    status: Status.none,\n  },\n  {\n    item: \"Go skydiving\",\n    status: Status.completed,\n  },\n  {\n    item: \"Solo backpack across Europe\",\n    status: Status.completed,\n  },\n  {\n    item: \"Photograph an artist at the MSG\",\n    status: Status.none,\n  },\n  {\n    item: \"Open a restaurant\",\n    status: Status.none,\n  },\n  {\n    item: \"Drive across North America\",\n    status: Status.none,\n  },\n  {\n    item: \"Live in New York City\",\n    status: Status.completed,\n  },\n  {\n    item: \"Do a month+ long hike\",\n    status: Status.none,\n  },\n  {\n    item: \"Go on tour with an artist\",\n    status: Status.none,\n  },\n  {\n    item: \"Climb a large mountain\",\n    status: Status.completed,\n  },\n  {\n    item: \"Help my parents retire\",\n    status: Status.progress,\n  },\n  {\n    item: \"Roadtrip with strangers\",\n    status: Status.completed,\n  },\n  {\n    item: \"Host a photo gallery\",\n    status: Status.progress,\n  },\n];\n\nexport const beliefs = [\n  \"Seek discomfort\",\n  \"Do difficult things as they are the most rewarding\",\n  \"Anything is possible with discipline\",\n];\n"
  },
  {
    "path": "hooks/useClickOutside.tsx",
    "content": "import { RefObject, useEffect } from \"react\";\n\nfunction useClickOutside<T extends HTMLElement>(\n  ref: RefObject<T | null>,\n  handler: (event: MouseEvent | TouchEvent) => void\n): void {\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent | TouchEvent) => {\n      if (!ref || !ref.current || ref.current.contains(event.target as Node)) {\n        return;\n      }\n\n      handler(event);\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    document.addEventListener(\"touchstart\", handleClickOutside);\n\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n      document.removeEventListener(\"touchstart\", handleClickOutside);\n    };\n  }, [ref, handler]);\n}\n\nexport default useClickOutside;\n"
  },
  {
    "path": "hooks/useMaxZIndex.tsx",
    "content": "\"use client\";\nimport { useState, useCallback } from \"react\";\n\nconst useMaxZIndex = () => {\n  const [zIndex, setZIndex] = useState(0);\n\n  const updateZIndex = useCallback(() => {\n    const els = document.querySelectorAll(\".drag-elements\");\n\n    let maxZIndex = Number.NEGATIVE_INFINITY;\n    for (let i = 0; i < els.length; i++) {\n      const el = els[i];\n      const zIndex = Number.parseInt(\n        window.getComputedStyle(el).getPropertyValue(\"z-index\")\n      );\n\n      if (!Number.isNaN(zIndex) && zIndex > maxZIndex) {\n        maxZIndex = zIndex;\n      }\n    }\n\n    setZIndex(maxZIndex + 1);\n  }, []);\n\n  return [zIndex, updateZIndex] as const;\n};\n\nexport default useMaxZIndex;\n"
  },
  {
    "path": "lib/blog-posts.ts",
    "content": "export interface BlogPost {\n  slug: string;\n  title: string;\n  description: string;\n  datePublished: string;\n  image?: string;\n}\n\nexport const blogPosts: BlogPost[] = [\n  {\n    slug: \"2025\",\n    title: \"[Annual Review] 2025\",\n    description:\n      \"A year in review: moving, adjusting, and finding breathing room\",\n    datePublished: \"2025-12-31\",\n  },\n  {\n    slug: \"2024\",\n    title: \"[Annual Review] 2024\",\n    description: \"Everything I want.\",\n    datePublished: \"2024-12-31\",\n  },\n  {\n    slug: \"q1-2025\",\n    title: \"[Quarter Review] Q1/25\",\n    description: \"Thriving on chaos\",\n    datePublished: \"2025-03-31\",\n  },\n  {\n    slug: \"q2-2025\",\n    title: \"[Quarter Review] Q2/25\",\n    description: \"Moving to NYC\",\n    datePublished: \"2025-06-30\",\n  },\n  {\n    slug: \"q3-2025\",\n    title: \"[Quarter Review] Q3/25\",\n    description: \"Three months in NYC: Starting from scratch\",\n    datePublished: \"2025-09-30\",\n  },\n];\n\nexport function getBlogPost(slug: string): BlogPost | undefined {\n  return blogPosts.find((post) => post.slug === slug);\n}\n"
  },
  {
    "path": "lib/literal.ts",
    "content": "interface Book {\n  slug: string;\n  title: string;\n  authors: { name: string }[];\n  cover: string;\n}\n\nconst LITERAL_ENDPOINT = \"https://api.literal.club/graphql\";\n\nconst getAccessToken = async () => {\n  const response = await fetch(LITERAL_ENDPOINT, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      query: `\n            mutation login($email: String!, $password: String!) {\n                login(email: $email, password: $password) {\n                    token\n                }\n            }\n        `,\n      variables: {\n        email: process.env.LITERAL_USER_EMAIL,\n        password: process.env.LITERAL_USER_PASSWORD,\n      },\n    }),\n  });\n\n  if (!response.ok) {\n    throw new Error(`Literal API error: ${response.status}`);\n  }\n\n  const data = await response.json();\n  return data.data.login.token;\n};\n\nexport const getShelves = async () => {\n  try {\n    const access_token = await getAccessToken();\n\n    const response = await fetch(LITERAL_ENDPOINT, {\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${access_token}`,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        query: `\n          query myReadingStates {\n            myReadingStates {\n              ...ReadingStateParts # find fragments below\n              book {\n                ...BookParts # find fragments below\n              }\n            }\n          }\n\n          fragment ReadingStateParts on ReadingState {\n            id\n            status\n            bookId\n            profileId\n            createdAt\n          }\n\n          fragment BookParts on Book {\n            id\n            slug\n            title\n            subtitle\n            description\n            cover\n            authors {\n              id\n              name\n            }\n          }\n            `,\n      }),\n    });\n\n    if (!response.ok) throw new Error(\"Something went wrong\");\n\n    const { data } = await response.json();\n\n    const latestBook = data.myReadingStates\n      .filter(\n        (shelf: { status: string; book: Book }) => shelf.status === \"IS_READING\"\n      )\n      .slice(-1)\n      .map(({ book }: { book: Book }) => ({\n        slug: book.slug,\n        title: book.title,\n        author: book.authors[0].name,\n        cover: book.cover,\n      }))[0];\n\n    return {\n      reading: latestBook,\n    };\n  } catch (error) {\n    console.error(\"Failed to fetch from Literal:\", error);\n    return { reading: {\n      slug: \"kitchen-confidential-updated-ed-p2gbl\",\n      title: \"Kitchen Confidential\",\n      author: \"Anthony Bourdain\",\n      cover: \"https://assets.literal.club/2/ckwdtpcpj46828913h1bfxfouew.jpg?size=600\",\n    } };\n  }\n};\n"
  },
  {
    "path": "lib/openai.ts",
    "content": "\"use server\";\n\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\nasync function moderateText(text: string) {\n  const moderation = await openai.moderations.create({\n    model: \"omni-moderation-latest\",\n    input: text,\n  });\n\n  const safe = moderation.results[0].flagged === false;\n  if (!safe) {\n    console.log(\"Moderation failed\");\n  }\n\n  return safe;\n}\n\nexport default moderateText;\n"
  },
  {
    "path": "lib/redis.ts",
    "content": "import { Redis } from \"ioredis\";\n\nconst redis = new Redis(process.env.REDIS_URL ?? \"\");\n\nexport default redis;\n"
  },
  {
    "path": "lib/spotify.ts",
    "content": "const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;\nconst CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;\nconst REFRESH_TOKEN = process.env.SPOTIFY_REFRESH_TOKEN;\n\nconst BASIC = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString(\"base64\");\nconst NOW_PLAYING_ENDPOINT =\n  \"https://api.spotify.com/v1/me/player/currently-playing\";\nconst RECENTLY_PLAYED_ENDPOINT =\n  \"https://api.spotify.com/v1/me/player/recently-played\";\nconst TOKEN_ENDPOINT = \"https://accounts.spotify.com/api/token\";\n\nconst getAccessToken = async () => {\n  const response = await fetch(TOKEN_ENDPOINT, {\n    method: \"POST\",\n    headers: {\n      Authorization: `Basic ${BASIC}`,\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\n    },\n    body: new URLSearchParams({\n      grant_type: \"refresh_token\",\n      refresh_token: REFRESH_TOKEN ?? \"\",\n    }),\n  });\n\n  const data = await response.json();\n  return data.access_token;\n};\n\nconst fetchSpotifyData = async (endpoint: string) => {\n  const accessToken = await getAccessToken();\n\n  const response = await fetch(endpoint, {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n    },\n  });\n\n  if (response.status === 204) {\n    return {\n      status: response.status,\n    };\n  }\n\n  try {\n    const data = await response.json();\n    return { status: response.status, data };\n  } catch {\n    return { status: response.status };\n  }\n};\n\nexport const getSpotifyData = async () => {\n  const nowPlaying = await fetchSpotifyData(NOW_PLAYING_ENDPOINT);\n  if (nowPlaying.status === 200 && nowPlaying.data.is_playing) {\n    return {\n      type: \"now-playing\",\n      data: nowPlaying.data,\n    };\n  }\n\n  const lastPlayer = await fetchSpotifyData(RECENTLY_PLAYED_ENDPOINT);\n  return {\n    type: \"last-played\",\n    data: lastPlayer.data,\n  };\n};\n\nexport const getNowPlaying = async () => {\n  const result = await getSpotifyData();\n  return result;\n};\n\nexport const getTopTracks = async () => {\n  const accessToken = await getAccessToken();\n\n  const response = await fetch(\n    \"https://api.spotify.com/v1/me/top/tracks?time_range=short_term&limit=20\",\n    {\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n      },\n      next: {\n        revalidate: 60,\n      },\n    }\n  );\n\n  if (response.status === 204) {\n    return {\n      status: response.status,\n    };\n  }\n\n  try {\n    const song = await response.json();\n\n    return {\n      status: response.status,\n      data: song,\n    };\n  } catch {\n    return {\n      status: response.status,\n    };\n  }\n};\n\nexport const getSeveralTracksFeatures = async (trackIDs: string[]) => {\n  const accessToken = await getAccessToken();\n\n  const response = await fetch(\n    `https://api.spotify.com/v1/audio-features?ids=${trackIDs.join(\",\")}`,\n    {\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n      },\n      next: {\n        revalidate: 60,\n      },\n    }\n  );\n\n  if (response.status === 204) {\n    return {\n      status: response.status,\n    };\n  }\n\n  try {\n    const song = await response.json();\n\n    return {\n      status: response.status,\n      data: song,\n    };\n  } catch {\n    return {\n      status: response.status,\n    };\n  }\n};\n\nexport default getNowPlaying;\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport function pick(object: Record<string, any>, keys: string[]) {\n  return keys.reduce((obj, key) => {\n    if (object && object.hasOwnProperty(key)) {\n      obj[key] = object[key];\n    }\n    return obj;\n  }, {} as Record<string, any>);\n}\n\nexport const getRandomRotation = () => {\n  const isNegative = Math.random() < 0.5;\n  const angle = Math.floor(Math.random() * 60);\n  return isNegative ? -angle : angle;\n};\n\nexport function formatDate(date: Date) {\n  const day = String(date.getDate()).padStart(2, \"0\");\n  const month = String(date.getMonth() + 1).padStart(2, \"0\");\n  const year = String(date.getFullYear()).slice(-2);\n\n  return `${day}/${month}/${year}`;\n}\n"
  },
  {
    "path": "mdx-components.tsx",
    "content": "import React, { type ComponentPropsWithoutRef } from \"react\";\nimport type { MDXComponents } from \"mdx/types\";\nimport Link from \"next/link\";\n\ntype HeadingProps = ComponentPropsWithoutRef<\"h1\">;\ntype ParagraphProps = ComponentPropsWithoutRef<\"p\">;\ntype ListProps = ComponentPropsWithoutRef<\"ul\">;\ntype ListItemProps = ComponentPropsWithoutRef<\"li\">;\ntype AnchorProps = ComponentPropsWithoutRef<\"a\">;\ntype BlockquoteProps = ComponentPropsWithoutRef<\"blockquote\">;\ntype ImageProps = ComponentPropsWithoutRef<\"img\">;\n\nconst components: MDXComponents = {\n  h1: (props: HeadingProps) => (\n    <h1 className=\"font-medium pt-8 mb-6 fade-in\" {...props} />\n  ),\n  h2: (props: HeadingProps) => (\n    <h2 className=\"text-[18px] font-medium mt-8 mb-3\" {...props} />\n  ),\n  h3: (props: HeadingProps) => (\n    <h3 className=\" font-medium mt-8 mb-3\" {...props} />\n  ),\n  h4: (props: HeadingProps) => <h4 className=\"font-medium\" {...props} />,\n  p: (props: ParagraphProps) => <p className=\"leading-snug\" {...props} />,\n  ol: (props: ListProps) => (\n    <ol className=\" list-decimal pl-5 space-y-2\" {...props} />\n  ),\n  ul: (props: ListProps) => (\n    <ul className=\" list-disc pl-5 space-y-1\" {...props} />\n  ),\n  li: (props: ListItemProps) => <li className=\"pl-1\" {...props} />,\n  em: (props: ComponentPropsWithoutRef<\"em\">) => (\n    <em className=\"font-medium\" {...props} />\n  ),\n  strong: (props: ComponentPropsWithoutRef<\"strong\">) => (\n    <strong className=\"font-medium\" {...props} />\n  ),\n  hr: (props: ComponentPropsWithoutRef<\"hr\">) => (\n    <hr className=\"border-t border-[#858380]/30 my-6\" {...props} />\n  ),\n  img: (props: ImageProps) => {\n    if (props.title !== undefined) {\n      return (\n        <figure>\n          <img\n            className=\"rounded-4 border border-gray-6\"\n            src={props.src}\n            alt={props.alt}\n            fetchPriority=\"high\"\n          />\n          <figcaption className=\"text-sm text-gray-11 mt-1.5\">\n            {props.title}\n          </figcaption>\n        </figure>\n      );\n    }\n    return (\n      <img\n        className=\"rounded-4 border border-gray-6\"\n        src={props.src}\n        alt={props.alt}\n      />\n    );\n  },\n  a: ({ href, children, ...props }: AnchorProps) => {\n    const className =\n      \"text-blue-10 hover:text-blue-11 hover:underline underline-offset-2\";\n    if (href?.startsWith(\"/\")) {\n      return (\n        <Link href={href} className={className} {...props}>\n          {children}\n        </Link>\n      );\n    }\n    if (href?.startsWith(\"#\")) {\n      return (\n        <a href={href} className={className} {...props}>\n          {children}\n        </a>\n      );\n    }\n    return (\n      <a\n        href={href}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className={className}\n        {...props}\n      >\n        {children}\n      </a>\n    );\n  },\n  blockquote: (props: BlockquoteProps) => (\n    <blockquote\n      className=\"ml-[0.075em] border-l-3 border-gray-300 pl-4\"\n      {...props}\n    />\n  ),\n};\n\nexport function useMDXComponents(\n  otherComponents: MDXComponents\n): MDXComponents {\n  return {\n    ...otherComponents,\n    ...components,\n  };\n}\n"
  },
  {
    "path": "microfrontends.json",
    "content": "{\n  \"$schema\": \"https://openapi.vercel.sh/microfrontends.json\",\n  \"applications\": {\n    \"mitul-ca\": {\n      \"development\": {\n        \"fallback\": \"mitul.ca\"\n      }\n    },\n    \"os-2\": {\n      \"routing\": [\n        {\n          \"group\": \"main\",\n          \"paths\": [\"/os\", \"/os/:path*\"]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "next.config.mjs",
    "content": "import createMDX from \"@next/mdx\";\nimport { withMicrofrontends } from \"@vercel/microfrontends/next/config\";\n\nconst withMDX = createMDX({});\n\nconst config = {\n  pageExtensions: [\"mdx\", \"ts\", \"tsx\"],\n  experimental: {\n    mdxRs: true,\n    cacheComponents: true,\n  },\n  // async redirects() {\n  //   return [\n  //     {\n  //       source: \"/about\",\n  //       destination: \"/os\",\n  //       permanent: false,\n  //     },\n  //   ];\n  // },\n  // async rewrites() {\n  //   return [\n  //     {\n  //       source: \"/os\",\n  //       destination: \"https://os-2.vercel.app/os\",\n  //     },\n  //     {\n  //       source: \"/os/:path*\",\n  //       destination: \"https://os-2.vercel.app/os/:path*\",\n  //     },\n  //   ];\n  // },\n\n  images: {\n    remotePatterns: [\n      {\n        protocol: \"https\",\n        port: \"\",\n        hostname: \"res.craft.do\",\n      },\n      {\n        protocol: \"https\",\n        port: \"\",\n        hostname: \"i.scdn.co\",\n      },\n      {\n        protocol: \"https\",\n        port: \"\",\n        hostname: \"assets.literal.club\",\n      },\n      {\n        protocol: \"https\",\n        port: \"\",\n        hostname: \"inqeleafibjx2dzc.public.blob.vercel-storage.com\",\n      },\n      {\n        protocol: \"https\",\n        port: \"\",\n        hostname: \"img.youtube.com\",\n      },\n    ],\n    qualities: [5, 25, 40, 75],\n  },\n};\n\nexport default withMicrofrontends(withMDX(config));\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"mitul-ca\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/anthropic\": \"^2.0.1\",\n    \"@ai-sdk/openai\": \"^2.0.5\",\n    \"@mdx-js/loader\": \"^3.1.0\",\n    \"@mdx-js/react\": \"^3.1.0\",\n    \"@next/mdx\": \"16.0.1\",\n    \"@phosphor-icons/react\": \"^2.0.13\",\n    \"@radix-ui/colors\": \"^3.0.0\",\n    \"@radix-ui/react-accordion\": \"^1.2.12\",\n    \"@radix-ui/react-collapsible\": \"^1.1.12\",\n    \"@radix-ui/react-hover-card\": \"^1.1.15\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.10\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@uiw/react-signature\": \"^1.3.1\",\n    \"@vercel/analytics\": \"^1.1.1\",\n    \"@vercel/microfrontends\": \"^2.2.1\",\n    \"@vercel/postgres\": \"^0.9.0\",\n    \"@vercel/speed-insights\": \"^1.0.12\",\n    \"bad-words\": \"^3.0.4\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"ioredis\": \"5.4.1\",\n    \"jotai\": \"^2.9.3\",\n    \"motion\": \"^12.23.12\",\n    \"ms\": \"^2.1.3\",\n    \"next\": \"16.0.10\",\n    \"next-themes\": \"^0.4.4\",\n    \"openai\": \"^6.33.0\",\n    \"p5\": \"^1.11.2\",\n    \"react\": \"19.2.0\",\n    \"react-dom\": \"19.2.0\",\n    \"react-use-measure\": \"^2.1.1\",\n    \"resend\": \"^4.0.1\",\n    \"swr\": \"^2.3.4\",\n    \"tailwind-merge\": \"^2.0.0\",\n    \"use-sound\": \"^4.0.1\",\n    \"zod\": \"^4.0.15\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.0.0-beta.4\",\n    \"@types/bad-words\": \"^3.0.3\",\n    \"@types/ms\": \"^0.7.34\",\n    \"@types/node\": \"^20.17.9\",\n    \"@types/p5\": \"^1.7.6\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"16.0.1\",\n    \"postcss\": \"^8\",\n    \"tailwindcss\": \"^4.0.0\",\n    \"typescript\": \"^5.7.2\"\n  },\n  \"overrides\": {\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n}\n"
  },
  {
    "path": "proxy.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\nfunction parseMediaType(mt: string): { type: string; quality: number } {\n  const parts = mt.trim().split(\";\");\n  const type = parts[0].trim().toLowerCase();\n  let quality = 1;\n\n  for (let i = 1; i < parts.length; i++) {\n    const param = parts[i].trim();\n    if (param.startsWith(\"q=\")) {\n      quality = parseFloat(param.slice(2)) || 0;\n    }\n  }\n\n  return { type, quality };\n}\n\nfunction shouldServeMarkdown(acceptHeader: string | null): boolean {\n  if (!acceptHeader) return false;\n\n  type MediaInfo = { quality: number; position: number };\n\n  const found: Record<string, MediaInfo> = {};\n  const relevantTypes = [\"text/html\", \"text/markdown\", \"text/plain\"];\n\n  acceptHeader.split(\",\").forEach((mt, i) => {\n    const { type, quality } = parseMediaType(mt);\n    if (relevantTypes.includes(type) && !found[type]) {\n      found[type] = { quality, position: i };\n    }\n  });\n\n  const html = found[\"text/html\"];\n  const markdown = found[\"text/markdown\"];\n  const plain = found[\"text/plain\"];\n\n  if (!markdown && !plain) return false;\n  if (!html) return true;\n\n  const preferHigherQuality = (a: MediaInfo, b: MediaInfo): MediaInfo => {\n    if (a.quality !== b.quality) {\n      return a.quality > b.quality ? a : b;\n    }\n    return a.position < b.position ? a : b;\n  };\n\n  let best: MediaInfo;\n  if (!markdown) {\n    best = plain;\n  } else if (!plain) {\n    best = markdown;\n  } else {\n    best = preferHigherQuality(markdown, plain);\n  }\n\n  return preferHigherQuality(best, html) === best;\n}\n\nconst LLM_USER_AGENTS = [\n  \"GPTBot\",\n  \"ChatGPT-User\",\n  \"Claude-Web\",\n  \"Claudebot\",\n  \"Anthropic\",\n  \"CCBot\",\n  \"PerplexityBot\",\n  \"Google-Extended\",\n  \"Bytespider\",\n  \"cohere-ai\",\n];\n\nfunction isLLMAgent(userAgent: string | null): boolean {\n  if (!userAgent) return false;\n  return LLM_USER_AGENTS.some((agent) =>\n    userAgent.toLowerCase().includes(agent.toLowerCase())\n  );\n}\n\nexport function proxy(request: NextRequest) {\n  const userAgent = request.headers.get(\"user-agent\");\n  const acceptHeader = request.headers.get(\"accept\");\n  const { pathname } = request.nextUrl;\n\n  // Auth check for /visitors/gang routes\n  const isAuthenticated = request.cookies.get(\"auth\");\n  if (!isAuthenticated && pathname.startsWith(\"/visitors/gang\")) {\n    return NextResponse.redirect(new URL(\"/visitors/login\", request.url));\n  }\n\n  // Skip for static files, API routes, and already markdown routes\n  if (\n    pathname.startsWith(\"/_next\") ||\n    pathname.startsWith(\"/api\") ||\n    pathname.startsWith(\"/md\") ||\n    pathname.includes(\".\") // static files like .ico, .png, etc.\n  ) {\n    return NextResponse.next();\n  }\n\n  // Serve markdown if Accept header prefers it, or if it's an LLM agent\n  if (shouldServeMarkdown(acceptHeader) || isLLMAgent(userAgent)) {\n    const url = request.nextUrl.clone();\n    url.pathname = \"/api/md\";\n    url.searchParams.set(\"path\", pathname);\n\n    const response = NextResponse.rewrite(url);\n    response.headers.set(\"x-original-path\", pathname);\n    return response;\n  }\n\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: [\n    \"/visitors/gang/:path*\",\n    \"/((?!_next/static|_next/image|favicon.ico|.*\\\\.).*)\",\n  ],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  }
]