# iOS App Store Today

> Story cards, full-screen expansion and a Get → progress → Open button.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/templates/ios-app-store-today
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/ios-app-store-today.json
```

Adds the page at `app/ios-app-store-today/page.tsx` together with the components below. Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Components

- [iOS Kit](https://chromotion.dev/r/ios-kit.md): `@/components/ios-kit`

## Dependencies

```bash
npm install lucide-react
```

## Source

```tsx title="app/ios-app-store-today/page.tsx"
"use client"

import * as React from "react"
import { Gamepad2, Layers, LayoutGrid, Rocket, Search, Sun, X } from "lucide-react"

import { cn } from "@/lib/utils"
import { IOSDevice, IOSPage, IOSTabBar, type IOSTabItem } from "@/components/ios-kit"

const tabs: IOSTabItem[] = [
  { id: "today", label: "Today", icon: Sun },
  { id: "games", label: "Games", icon: Rocket },
  { id: "apps", label: "Apps", icon: Layers },
  { id: "arcade", label: "Arcade", icon: Gamepad2 },
  { id: "search", label: "Search", icon: Search },
]

const stories = [
  {
    id: "focus",
    kicker: "THE DAILY LIST",
    title: "Apps that protect your focus",
    blurb: "Quiet tools that keep notifications, tabs and tasks in their place.",
    body: "Deep work starts with removing friction. These five apps hide distractions, batch your notifications and turn a messy day into a short, clear list. Start with one, keep it for a week, then decide whether it earns a place on your Home Screen.",
    gradient: "from-indigo-500 via-violet-600 to-fuchsia-600",
  },
  {
    id: "sunrise",
    kicker: "WORLD PREMIERE",
    title: "Sunrise Run: a game made for mornings",
    blurb: "Five-minute levels, a calm soundtrack and no timers.",
    body: "Sunrise Run turns your commute into a gentle adventure. Levels are short, progress is saved instantly, and there are no streak penalties. It's the rare mobile game that is happy when you put it down.",
    gradient: "from-amber-400 via-orange-500 to-rose-500",
  },
  {
    id: "studio",
    kicker: "GET STARTED",
    title: "Make your first song in ten minutes",
    blurb: "Beat makers, loop pads and mixers that feel like instruments.",
    body: "You don't need to read music to start. Pick a loop pack, tap in a rhythm, layer a bassline and export. These apps are designed to get you from zero to a finished track before your coffee cools.",
    gradient: "from-emerald-400 via-teal-500 to-sky-600",
  },
]

const apps = [
  { name: "Focusly", sub: "Distraction blocker", color: "from-indigo-400 to-violet-600" },
  { name: "Sprout", sub: "Habit garden", color: "from-lime-400 to-emerald-600" },
  { name: "Pocket Chef", sub: "Recipes that adapt", color: "from-orange-400 to-red-500" },
  { name: "Lumen", sub: "Photo editor", color: "from-sky-400 to-blue-600" },
]

type Phase = "get" | "loading" | "open"

function GetButton({ name }: { name: string }) {
  const [phase, setPhase] = React.useState<Phase>("get")
  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null)

  React.useEffect(() => () => {
    if (timer.current) clearTimeout(timer.current)
  }, [])

  const press = () => {
    if (phase !== "get") return
    setPhase("loading")
    timer.current = setTimeout(() => setPhase("open"), 1600)
  }

  return (
    <button
      type="button"
      onClick={press}
      aria-label={`${phase === "open" ? "Open" : "Get"} ${name}`}
      className="relative flex h-8 min-w-[72px] items-center justify-center rounded-full bg-(color:--ios-fill) px-4 text-[15px] font-bold text-(color:--ios-blue) active:opacity-60"
    >
      {phase === "loading" ? (
        <svg viewBox="0 0 24 24" className="size-6 -rotate-90" aria-label="Downloading">
          <circle cx="12" cy="12" r="9" fill="none" strokeWidth="2.5" className="stroke-(color:--ios-label3)" />
          <circle cx="12" cy="12" r="9" fill="none" strokeWidth="2.5" strokeLinecap="round" className="stroke-(color:--ios-blue) animate-[ios-ring_1.6s_linear_forwards]" strokeDasharray="56.5" strokeDashoffset="56.5" />
        </svg>
      ) : phase === "open" ? (
        "OPEN"
      ) : (
        "GET"
      )}
    </button>
  )
}

export default function IOSAppStoreToday() {
  const [tab, setTab] = React.useState("today")
  const [openId, setOpenId] = React.useState<string | null>(null)
  const story = stories.find((item) => item.id === openId)

  return (
    <IOSDevice>
      <style>{`@keyframes ios-ring{to{stroke-dashoffset:0}}`}</style>
      <IOSPage
        title="Today"
        tabbed
        right={<span className="mr-1 flex size-8 items-center justify-center rounded-full bg-gradient-to-br from-sky-400 to-indigo-600 text-[13px] font-semibold text-white">A</span>}
      >
        {tab !== "today" ? (
          <div className="flex flex-col items-center gap-3 px-8 pt-24 text-center text-(color:--ios-label2)">
            <LayoutGrid className="size-10" />
            <p className="text-[17px]">The {tabs.find((t) => t.id === tab)?.label} tab is not part of this template.</p>
          </div>
        ) : (
          <>
            <p className="-mt-2 px-5 pb-3 text-[13px] font-semibold text-(color:--ios-label2) uppercase">Friday, November 6</p>
            <ul className="flex flex-col gap-5 px-4">
              {stories.map((s) => (
                <li key={s.id}>
                  <button
                    type="button"
                    onClick={() => setOpenId(s.id)}
                    className={cn(
                      "relative flex h-[420px] w-full flex-col justify-between overflow-hidden rounded-[22px] bg-gradient-to-br p-5 text-left text-white shadow-[0_10px_30px_rgb(0_0_0/0.2)] transition-transform duration-200 active:scale-[0.97]",
                      s.gradient
                    )}
                  >
                    <span>
                      <span className="block text-[13px] font-semibold tracking-wide opacity-80">{s.kicker}</span>
                      <span className="mt-1 block text-[32px] leading-[36px] font-bold">{s.title}</span>
                    </span>
                    <span className="-mx-5 -mb-5 block bg-black/25 px-5 py-3 text-[14px] backdrop-blur-xl">{s.blurb}</span>
                  </button>
                </li>
              ))}
            </ul>

            <h2 className="px-5 pt-7 pb-1 text-[22px] font-bold">Apps We Love</h2>
            <ul className="mx-4 overflow-hidden rounded-[16px] bg-(color:--ios-surface)">
              {apps.map((app) => (
                <li key={app.name} className="relative flex items-center gap-3 px-3 py-2.5 after:absolute after:right-0 after:bottom-0 after:left-[72px] after:h-px after:bg-(color:--ios-sep) last:after:hidden">
                  <span className={cn("size-[60px] shrink-0 rounded-[14px] bg-gradient-to-br", app.color)} />
                  <span className="min-w-0 flex-1">
                    <span className="block truncate text-[17px] font-medium">{app.name}</span>
                    <span className="block truncate text-[14px] text-(color:--ios-label2)">{app.sub}</span>
                  </span>
                  <GetButton name={app.name} />
                </li>
              ))}
            </ul>
          </>
        )}
      </IOSPage>

      {story && (
        <div role="dialog" aria-modal="true" aria-label={story.title} className="absolute inset-0 z-40 flex animate-in flex-col overflow-y-auto bg-(color:--ios-bg) duration-300 fade-in slide-in-from-bottom-8 [scrollbar-width:none]">
          <div className={cn("relative flex h-[460px] shrink-0 flex-col justify-end bg-gradient-to-br p-5 pt-16 text-white", story.gradient)}>
            <button type="button" onClick={() => setOpenId(null)} aria-label="Close" className="absolute top-14 right-4 flex size-8 items-center justify-center rounded-full bg-black/40 backdrop-blur active:scale-90">
              <X className="size-4" />
            </button>
            <span className="text-[13px] font-semibold tracking-wide opacity-80">{story.kicker}</span>
            <h2 className="mt-1 text-[34px] leading-[38px] font-bold">{story.title}</h2>
          </div>
          <div className="space-y-4 px-5 py-6 text-[17px] leading-[24px]">
            <p className="font-semibold">{story.blurb}</p>
            <p className="text-(color:--ios-label2)">{story.body}</p>
            <p className="text-(color:--ios-label2)">Tap Get on any app in the list to try the download flow, then close this story to go back to Today.</p>
          </div>
        </div>
      )}

      <IOSTabBar items={tabs} value={tab} onValueChange={setTab} />
    </IOSDevice>
  )
}
```
