iArt.ai
AI motion graphics software — the After Effects alternative anyone can use. Create animations and explainer videos from a text prompt in minutes.
FRAMES is the home for landscape creators. Every Frame full-screen and cinematic — 16:9, 21:9, and 16:10.
MediaTypeScript · JavaScriptGitHub @collinchirwatv3-cpuOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
The cinematic social network. Not a TikTok clone — the test for every feature here is
"does this make watching video feel more cinematic?" FRAME is landscape-first, not
16:9-only — 16:9, 21:9 Cinema, and 16:10 are all first-class, with 3:2 and 4:3 wired
into the same taxonomy for future launch. Swipe-first, full-screen, built for
filmmakers, drone pilots, and anyone who shoots wide.Read the full README ↗The cinematic social network. Not a TikTok clone — the test for every feature here is "does this make watching video feel more cinematic?" FRAME is landscape-first, not 16:9-only — 16:9, 21:9 Cinema, and 16:10 are all first-class, with 3:2 and 4:3 wired into the same taxonomy for future launch. Swipe-first, full-screen, built for filmmakers, drone pilots, and anyone who shoots wide.
This repo is the Phase 1 MVP, now through its Version 2 transformation (see Version 2 — Cinematic Social Network below): the core swipe feed, upload flow with landscape enforcement, Explore/Discovery, Collections, profile, inbox, and auth scaffolding. No monetization, TV app, or AI features yet — see Roadmap.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) + TypeScript | RSC for fast first paint on feed/profile pages, route handlers double as the API layer |
| Styling | Tailwind CSS v4 | Design tokens live in globals.css via @theme, no separate config file to drift |
| Animation | Framer Motion | Shared motion vocabulary in lib/motion.ts — cinematic focus-pull, Director Mode chrome fade, sheet springs — nothing snaps |
| State | Zustand | One tiny global store (src/store/player-store.ts) for mute + active video; everything else is local component state |
| Data fetching | TanStack Query | Wired via QueryProvider, ready for real endpoints — feed currently reads mock data directly |
| Auth + DB | Supabase | Postgres for users/videos/follows/likes, @supabase/ssr for cookie-based sessions |
| Video delivery | Cloudflare Stream (recommended) | Adaptive HLS out of the box, cheaper per-minute than Mux at MVP scale, revisit Mux once creator analytics depth matters more than cost |
| Object storage | Cloudflare R2 | Avatars, banners, thumbnails, LUT/preset downloads — no egress fees pairs well with Stream |
npm install
cp .env.local.example .env.local # fill in Supabase + Cloudflare keys when ready
npm run dev
The app reads real Supabase data and requires the configured environment. Bundled
demo profiles, videos, and collections have been removed; empty accounts show
empty states. Uploads require Cloudflare Stream credentials. Category choices
live in src/lib/categories.ts. Private share-token playback remains unimplemented.
The retired seed identities are recorded in scripts/demo-manifest.json.
node --env-file=.env.local scripts/remove-demo-data.mjs previews those exact
records; --apply verifies identities, snapshots their content locally, then
removes them and associated cascading interactions. It refuses non-demo videos
or changed identities. This is not a general-purpose user cleanup command.
FRAME's identity changed: from "TikTok for landscape video" to a cinematic social network — every decision measured against "does this make watching video feel more cinematic?" This was a critique-then-transform pass, not a feature bolt-on. The full critique and prioritization is preserved in the session's plan file; the summary:
What was removed or softened (TikTok patterns that didn't fit the new identity):
ActionRail.tsx), not a separate
animated element flying up the screen.What's new:
player-store.ts (directorMode), rendered in VideoCard.tsx,
FeedTabs.tsx, SideRail.tsx, BottomNav.tsx; reset on feed unmount
(SwipeFeed.tsx) so it can never leak into other routes.scroll-snap-stop: always in globals.css keeps a
fast fling from skipping past multiple videos — "one scene at a time," not doomscroll
velocity.lib/audio.ts's fadeVolume() ramps volume via
requestAnimationFrame instead of snapping; video never begins or ends abruptly
once unmuted. Spatial Audio/Dolby Atmos need encode-side support Cloudflare Stream
doesn't expose today — that's a Phase 5 dependency, documented honestly rather than
faked client-side.VideoDetails in types.ts, VideoDetailsSheet.tsx),
reached by tapping "Shot details" under a caption rather than adding another icon
to an already-busy rail.Collection type, a cover-art rail
(CollectionsRail.tsx), a /collections/[id] detail page, and a real destination
for the Profile "Collections" tab (previously a dead stub) backed by a new
savedCollections set in engagement-store.ts, following the exact pattern
already used for likes/saves/follows.Deliberately not built yet (documented, not faked):
videos.type: 'vod' | 'live' is a natural reserved column whenever
this gets built; no code changes needed today to keep the door open.roadmap.md Milestone 5.A different mode from the last two rounds: "FRAME is no longer in feature development... Do not invent unnecessary features. Refine. Simplify. Polish." The full audit and reasoning behind every call here lives in BETA_READINESS.md — what got removed, what got improved, what was deliberately left alone, and two launch checklists. This section is the changelog; that file is the report.
Removed, not added — the point of this pass:
VideoOptionsSheet's "Copy link" action, which duplicated ActionRail's
dedicated Share button.VideoCard — redundant with Explore already being
one tap away in the persistent nav.Hidden Gems (lowest-like-count videos) in favor of New Voices (creators
below a follower threshold) — a real, distinct signal that serves creator
retention directly, unlike a rail that was really just re-sorting the same 5
videos. The brief's other four suggested rail types (Award Winners, Trending
Collections, Weekend Watchlist, Editor's Choice) were deliberately not
built — with 4 creators and 5 videos they'd have no real signal behind them,
which is exactly the "endless algorithmic clutter" the brief warns against.Fixed — broken promises and real friction:
app/(app)/settings) — interests, sound-on-by-default, sign out — and
onboarding gained a real "why FRAME exists" step instead of dropping straight
into a category picker.Category didn't support.upload-draft-store.ts autosaves title/description/category (metadata only —
a File object can't survive a reload without IndexedDB, which is out of
scope here; documented rather than half-built).Built — each tied to a specific brief section:
/watch/[id] — a real generateMetadata + opengraph-image.tsx
(Next's built-in ImageResponse, no external service) so a shared link
actually unfurls as a branded card (poster, title, creator, duration) in
iMessage/Slack/Twitter. ActionRail's Share button points here now instead of
the internal /?v= deep link, which stays as the in-app navigation mechanism.
Deliberately scoped to public videos only — /watch/[id] never looks at
privateVideos; a private video's only public surface remains the
token-gated, revocable /s/[token].lib/search.ts): matches category, location, camera, lens,
equipment, tags, and badges — not just title/username — and surfaces matching
Collections above the video grid, not just videos.lib/quality.ts): a 0–100 score from resolution, supported
aspect ratio, badge signals, and an engagement-per-follower proxy — never
shown as a number anywhere (per spec). It matters through one real
integration: FRAME Certified in computeBadges() can now be earned by score
as well as manually authored, so it's a live signal instead of a fixed list.
Honest gap: completion rate and watch retention (which the spec also asks
for) need real playback analytics this app doesn't have yet — the proxy is
named as a proxy, not disguised as the real thing.Premiere type in
types.ts documenting scheduled-release, reminder, and countdown-chat-window
semantics. No UI, no store, nothing wired — a schema for later, not a feature
now.<MotionConfig reducedMotion="user"> at the root layout —
every existing Framer Motion animation in the app now respects the OS-level
reduced-motion setting from one place, rather than needing prefers-reduced-motion
checks threaded through every component individually.Investigated, not changed — the brief asked whether comments should exist at
all, or whether something like "Creator Notes" would feel more intentional.
Worth noting: Creator Notes already exists — VideoDetails.creatorNotes,
shipped in Version 2, surfaced in VideoDetailsSheet. The existing
CommentDrawer is already an opt-in bottom sheet, not an always-visible endless
thread — it was never the pattern the brief worries about. Replacing a working,
expected feature with something unproven days before a creator beta is real risk
for no demonstrated upside, so it stays as-is. Full reasoning in
BETA_READINESS.md.
src/
app/
(app)/ route group with the shared nav shell
page.tsx Home — swipe feed
explore/
upload/
inbox/
profile/
onboarding/ interest picker, gates the (app) shell on first visit
login/ outside the shell (no bottom nav)
components/
feed/ SwipeFeed, VideoCard, FeedTabs, ActionRail, VideoOverlay,
CommentDrawer, VideoOptionsSheet
upload/ UploadDropzone + UploadRejection (multi-ratio gate + Rotate/Crop/Guidelines)
explore/ profile/ inbox/ nav/
ui/ Logo, Avatar, BadgeRow
lib/
aspect-ratio.ts the ratio taxonomy — single source of truth for classification
video-validation.ts upload accept/reject logic, built on aspect-ratio.ts
badges.ts merges editorial + computed badges (never asserts a ratio badge
the real pixels don't back up)
supabase/ browser + server clients, middleware session refresh
mock-data.ts types.ts
store/
player-store.ts mute + active-video
engagement-store.ts likes/saves/follows, persisted to localStorage
comments-store.ts locally-added comments, persisted to localStorage
onboarding-store.ts first-visit interest picker, persisted to localStorage
SwipeFeed is a native CSS scroll-snap container (snap-y snap-mandatory), not a
custom drag/swipe reimplementation — that gets momentum scrolling, rubber-banding,
and mouse-wheel-advances-to-next-video for free, which also satisfies the desktop
"wheel scrolls to next video" requirement without extra code. An IntersectionObserver
tracks which card is ≥60% visible and marks it active; only the active <video>
plays, everything else is paused and reset — critical for battery/bandwidth once this
is real HLS instead of mock MP4s.
Where I pushed back on the brief: "every video is 16:9" and "no black bars" are in tension on a portrait phone screen — a 16:9 video literally cannot fill a 9:19.5 viewport without either cropping the frame or leaving empty space above/below it. Cropping would violate "every video is 16:9" (you'd be showing a different crop than the creator uploaded). So instead of hard black bars, the letterbox space is filled with a blurred, scaled-up copy of the video's poster frame — the Apple TV app / Netflix mobile technique. It reads as intentional and cinematic rather than as an unfilled gap, and the actual frame the creator shot is never cropped or stretched.
lib/aspect-ratio.ts defines the taxonomy as five non-overlapping ratio bands
(minRatio–maxRatio, not a single nominal value) so real-world encodes classify
correctly — e.g. 21:9 in the spec is nominally 2.33:1, but the actual example
resolutions (2560×1080, 3440×1440, 5120×2160) range 2.37–2.39, so the band is
[2.20, 2.45]. 16:9/21:9/16:10 are enabled: true (Primary Support); 3:2/4:3 are
enabled: false (Optional Future Support) — flipping that one flag is the entire
launch step for either, since classification, badges, and the discovery filter all
key off the same table. Gaps between bands (e.g. 1.85:1 "flat" cinema) are
intentionally unsupported until added here, not silently rounded into a neighbor.
The player needed almost no changes. It renders <video className="w-full h-full object-contain"> inside a full-bleed centered flex box, sized from the video's real
intrinsic pixels — never a hardcoded 16:9 box. That means 21:9 letterboxes and 16:10
pillarboxes for free, using the exact same blurred-backdrop technique already built
for 16:9. The only real player change was constraining the overlay chrome (captions,
action rail, tabs) to a max-w-[1920px] centered column so it stays visually anchored
to the video instead of floating at the edges of an ultra-wide monitor.
Grid thumbnails respect the real ratio too. TrendingGrid (Explore/Profile) used
to be a fixed aspect-video CSS grid; it's now a CSS-columns masonry layout with each
card sized via style={{ aspectRatio: width/height }} — a 21:9 upload genuinely reads
as a wide card, not a 16:9 box with a cropped thumbnail. Pure CSS, no JS masonry library.
UploadDropzone loads the dropped file into an off-DOM <video> element, reads
videoWidth/videoHeight from loadedmetadata, and calls checkUpload() before
anything reaches a form or an upload request. Two distinct rejection paths, because
they warrant different copy and different recovery options:
not-landscape (portrait or square) → UploadRejection explains FRAME is
landscape-only and offers three real options: Rotate 90° (genuinely swaps
width/height and re-validates — this is the actual fix for the common "phone
recorded sideways, exported portrait" case, not just a message), Crop to a
supported ratio (shows a real centered-crop preview per target ratio, nothing
applied until confirmed — the spec is explicit: do not auto-crop), and
guidelines (the supported ratios + example resolutions, pulled from the same
ASPECT_RATIOS table so it can't drift from what's actually enforced).unsupported-ratio (landscape, but outside every enabled band — e.g. 1.85:1)
→ softer copy, plus the nearest enabled ratio by distance from the closer band edge.The upload screen shows aspect ratio, resolution, and duration immediately — a
browser <video> element can read those from loadedmetadata. FPS, codec, and
bitrate are shown as "detected after upload" rather than faked: the browser's
media APIs don't expose them reliably cross-browser, and in the real pipeline they
only become available once Cloudflare Stream finishes probing the file server-side.
Showing placeholder values here would be lying about what the client actually knows.
lib/badges.ts computes a video's badge list by merging two sources: editorial/
creator-declared badges (video.badges — Drone, Shot on RED, FRAME Certified) and
badges computed from the real encode (4K if the resolution qualifies, 21:9 Cinema
if classifyAspectRatio says so). This split matters: a "21:9 Cinema" badge can
never be manually asserted on a video that doesn't actually play back that wide —
it's derived, not editable. Known gap: all current mock videos are genuinely
16:9 (they're reused stock demo clips), so the 21:9/16:10 Explore filters and the
21:9 Cinema badge are correctly empty in this build — they'll populate the moment
a real widescreen file is uploaded, since validation runs against real probed
dimensions, not mock data.
A creator's Profile has a third tab, Private, alongside Videos and Collections
(ProfileTabs.tsx). Private videos (mock-data.ts's privateVideos) never appear
in the feed, Explore, or the public profile grid — the only way to reach one is a
share link the creator generates from that tab (CreateShareLinkSheet.tsx), with a
chosen TTL (1 hour / 24 hours / 7 days). The link resolves at /s/[token], outside
the app shell, and:
ExpiredLinkNotice.tsx).Revocation is immediate and independent of the expiry timer — a creator can kill a link early from the same sheet that created it, and each link tracks a basic view count (incremented once per valid page load).
Real implementation vs. this build: lib/share-links.ts documents the intended
shape — a server-minted, DB-backed token (not a client-generated one) checked
against a share_links table on every request, with Cloudflare Stream's own
signed-URL/requireSignedURLs feature layered underneath to control actual
playback access (short-lived, minted fresh per page load, independent of the
share link's own longer TTL). In this Phase 1 build, share-links-store.ts is
localStorage-backed like the other engagement stores — which means a link only
resolves in the same browser that created it. That's enough to demo and test the
full creation → view → expiry → revocation flow, but it does not yet work for
actually sending a link to someone else's device — that requires Milestone 1 of
roadmap.md.
No live database exists yet in Phase 1 (state lives in mock-data.ts and
localStorage-backed Zustand stores) — this documents the schema the current
types already assume, so Phase 2's actual Postgres migration doesn't have to
re-derive it:
videos
id, creator_id, playback_url, poster_url, title, description, category,
width int, height int, -- source of truth for aspect ratio; never store
-- a separate "aspect_ratio" enum column, derive
-- it from width/height like the client does
visibility enum('public','private'), -- private videos are excluded from every
-- feed/Explore/profile-grid query, full stop —
-- reachable only via a valid share_links row
badges text[], -- editorial only (Drone, Shot on RED, FRAME Certified);
-- computed badges (4K, 21:9 Cinema) stay derived,
-- never written to a row
duration_seconds, sound_name, likes/comments/shares/saves counters,
created_at
creators
id, username, display_name, avatar_url, banner_url, bio, website,
verified bool, followers/following/total_views (denormalized counters
or materialized views once real-time accuracy matters)
follows (user_id, creator_id)
likes (user_id, video_id)
saves (user_id, video_id)
comments (id, video_id, user_id, text, created_at)
watch_progress (user_id, video_id, position_seconds) -- Phase 4, cross-device resume
share_links (token pk, video_id, creator_id, expires_at, revoked_at, view_count,
created_at) -- token is server-generated;
-- `/s/[token]` checks this
-- table, not client state
width/height as the aspect-ratio source of truth (rather than a stored ratio
label) is deliberate — it's the same principle lib/badges.ts relies on client-side:
derive, don't duplicate, so nothing can claim a ratio the pixels don't back up.
Updated 2026-09-14 — most of this section described a mock-data-only phase of the project that has since ended. Corrected below; only share links and crop confirmation are still genuinely stubbed.
/api/uploads/route.ts
mints a real Cloudflare Stream direct-upload (TUS) session, the client PUTs
the file straight to Cloudflare, and /api/webhooks/stream/route.ts
updates the video to ready once Stream confirms the encode (also where
the actual short/monetise-eligibility reclassification happens against
Stream's authoritative duration, closing a forged-client-duration bypass
fixed 2026-09-14). Caveat: CLOUDFLARE_STREAM_API_TOKEN has never
actually been provisioned with a real value — the code is complete but has
likely never run end-to-end against a real Stream account.UploadRejection's crop preview is a real, accurate centered-crop preview,
but "Continue cropped" doesn't cut pixels client-side — no canvas/WebCodecs
re-encode exists yet. It stages the decision and shows it back to the
uploader; the real crop is meant to happen server-side alongside
transcoding but does not yet./api/comments/route.ts), not a direct client insert. Onboarding
interests are still localStorage-only (onboarding-store.ts) and, as of
2026-09-14, are also not read by any feed-ranking code — the Settings copy
describing them as shaping Discover was corrected to stop claiming that.share-links-store.ts remains
localStorage-backed; a link literally cannot be opened on a different
device until it's backed by the real share_links table + Stream signed
URLs (see above). Still the
highest-priority remaining item in this section.Feature evolution after launch. For the checklist to actually get from this repo to a live production site, see roadmap.md.
The target audience is professional film people — working cinematographers, editors, and creators who care about proper aspect ratios and real technical craft, not the widest possible casual audience. "16:9 fanatics" is the literal framing: landscape-only by design (16:9/21:9/16:10, no portrait), and every UI decision from here on should read as professional film tooling that happens to be a social app, not a TikTok clone with a different crop.
Already-built work that's consistent with this direction, worth treating as
precedent rather than one-offs: the tag taxonomy's gear facet (real
manufacturer/model tags — Sony FX3, ARRI Alexa 35, specific lens/mount
combinations — with auto-inheritance, not a generic "camera" checkbox), the
multi-ratio landscape upload gate with rotate/crop recovery, the cinematic
dark visual language, and the clip-trim sheet's video-preview scrubber
(ClipCreateSheet.tsx) — a real, if minimal, NLE-style interaction rather
than a bare two-handle slider. Future UI work (the tag-picker dropdowns,
any further editing-adjacent surfaces) should keep measuring itself against
"would a working DP/editor find this credible" rather than "does this match
a generic social app pattern."
watch_progress table keyed by user+video, shared across mobile/desktop/TV.savedVideosengagement-store.tsCreator gained optional statement,
equipment, availableForHire; ProfileHeader.tsx shows them when present, and a
new FeaturedWork.tsx surfaces a creator's top video and a collection their work
appears in, above the tabs.lib/motion.ts centralizes duration/easing constants
(DURATION, EASE_OUT, SHEET_SPRING, FOCUS_PULL_TRANSITION,
CHROME_FADE_TRANSITION) so every animation reaches for the same considered
vocabulary instead of an inline spring config invented per component.