{"id":"vp0.forge-workout","docs":"This is an iOS app UI starter built with React Native. Paste the design link into your AI app builder (Claude Code, Rork, Lovable) and ask it to start from this design. Add your own backend/data logic. These are UI starters.","kind":"app_template","meta":{"com.vp0.kind":"app_template","com.vp0.category":"Fitness","com.vp0.platform":"ios","com.vp0.contentId":"fc9cccd0-3f0c-4312-bd8b-30093c4baf91","com.vp0.simulator":{"target":"react-native","embedUrl":null,"provider":null,"entrypoint":"App.tsx","buildStatus":null,"simulatable":true},"com.vp0.sourceHash":"sha256:95f9eed54140290c0511825d25355481cb05fedf662fa761b28a2715cff4e1b8","com.vp0.contentType":"template","com.vp0.sourceToken":"vp0_1f4d344d3691ccdcc03c7860","com.vp0.defaultLanguage":"react-native","com.vp0.availableLanguages":["react-native","swiftui"]},"name":"forge-workout","slug":"forge-workout","tags":["Dark","Fitness","Energetic"],"type":"registry:block","files":[{"path":"App.tsx","type":"registry:page","rawUrl":"https://api.vp0.com/designs/forge-workout/raw?language=react-native&path=App.tsx","target":"app/App.tsx","content":"import React, { useState, useEffect, createContext, useContext } from 'react';\nimport { View, Text, Pressable } from 'react-native';\nimport { ThemeProvider, useT, T, Screen, Scroll, Header, NavBar, Card, Group, Row, Button, Segmented, Chip, Pill, Checkbox, Stat, Ring, Bars, Spark, Progress, TabBar, Tabs, Stack, Icon, Spacer, SectionTitle, Gradient, alpha } from './ui/kit';\n\nconst ACCENT = '#F43F5E';\nconst THEME = { dark: { bg: '#0F0709', card: '#1B1014', card2: '#29191E', line: '#2E1E23', text2: '#B59AA0', text3: '#765C62' }, light: { bg: '#FBF1F3', card: '#FFFFFF', card2: '#F3DFE3' } };\nconst PLAN = [\n  { id: 1, name: 'Barbell squat', sets: 4, reps: 6, kg: 80, icon: 'Dumbbell', c: '#F43F5E' },\n  { id: 2, name: 'Bench press', sets: 4, reps: 8, kg: 60, icon: 'Dumbbell', c: '#F59E42' },\n  { id: 3, name: 'Deadlift', sets: 3, reps: 5, kg: 110, icon: 'Dumbbell', c: '#A78BFA' },\n  { id: 4, name: 'Pull-ups', sets: 3, reps: 10, kg: 0, icon: 'MoveUp', c: '#38BDF8' },\n  { id: 5, name: 'Plank', sets: 3, reps: 60, kg: 0, icon: 'Timer', c: '#22C55E' },\n];\nconst WEEK = [3200, 0, 4100, 2800, 0, 5200, 0];\nconst DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];\nconst Store = createContext(null);\n\nfunction TodayScreen({ nav }) {\n  const t = useT();\n  const { done } = useContext(Store);\n  const n = PLAN.filter((e) => done[e.id] >= e.sets).length;\n  const vol = PLAN.reduce((a, e) => a + (done[e.id] || 0) * e.reps * e.kg, 0);\n  return (\n    <Scroll pad={0}>\n      <Header kicker=\"Tuesday · Push day\" title=\"Strength A\" right={<Pill label=\"Week 6\" color={ACCENT} />} />\n      <View style={{ paddingHorizontal: 20 }}>\n        <Card pad={0} style={{ overflow: 'hidden' }} onPress={() => nav.push('session')}>\n          <Gradient colors={['#881337', '#F43F5E']} style={{ padding: 18 }}>\n            <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n              <View><Text style={[T.caption, { color: alpha('#fff', 0.85), textTransform: 'uppercase', letterSpacing: 1 }]}>Progress</Text><Text style={[T.title1, T.num, { color: '#fff' }]}>{n}/{PLAN.length} <Text style={[T.title3, { color: alpha('#fff', 0.85) }]}>exercises</Text></Text></View>\n              <View style={{ alignItems: 'flex-end' }}><Text style={[T.caption, { color: alpha('#fff', 0.85), textTransform: 'uppercase', letterSpacing: 1 }]}>Volume</Text><Text style={[T.title1, T.num, { color: '#fff' }]}>{(vol / 1000).toFixed(1)}t</Text></View>\n            </View>\n            <Progress value={n / PLAN.length} color=\"#fff\" track={alpha('#fff', 0.3)} style={{ marginTop: 16 }} />\n            <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 14 }}>\n              <Text style={[T.footnote, { color: alpha('#fff', 0.9) }]}>Est. 52 min · 420 kcal</Text>\n              <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}><Text style={[T.footnote, { color: '#fff', fontWeight: '700' }]}>Start session</Text><Icon name=\"ChevronRight\" size={16} color=\"#fff\" /></View>\n            </View>\n          </Gradient>\n        </Card>\n        <SectionTitle title=\"Exercises\" action=\"Edit\" onAction={() => {}} />\n        <Group>\n          {PLAN.map((e) => {\n            const d = done[e.id] || 0;\n            return <Row key={e.id} icon={e.icon} iconColor={e.c} iconBg={alpha(e.c, 0.2)} title={e.name} subtitle={e.sets + ' × ' + e.reps + (e.kg ? ' · ' + e.kg + ' kg' : e.id === 5 ? ' s' : ' · bodyweight')} right={<Ring progress={d / e.sets} size={34} stroke={4} color={e.c}><Text style={[T.caption2, T.num, { color: t.text }]}>{d}</Text></Ring>} chevron onPress={() => nav.push('exercise', { id: e.id })} />;\n          })}\n        </Group>\n      </View>\n    </Scroll>\n  );\n}\n\nfunction ProgressScreen() {\n  const t = useT();\n  const [range, setRange] = useState('4w');\n  return (\n    <Scroll pad={0}>\n      <Header kicker=\"Getting stronger\" title=\"Progress\" />\n      <View style={{ paddingHorizontal: 20 }}>\n        <Card>\n          <View style={{ flexDirection: 'row' }}><Stat label=\"This week\" value=\"15.3t\" delta=\"+1.8t\" /><Stat label=\"Sessions\" value=\"3/4\" /><Stat label=\"Streak\" value=\"6 wks\" color={ACCENT} /></View>\n          <Spacer h={16} />\n          <Bars values={WEEK} labels={DAYS} height={110} highlight={5} />\n        </Card>\n        <SectionTitle title=\"Estimated 1RM\" />\n        <Segmented options={[{ label: '4 weeks', value: '4w' }, { label: '3 months', value: '3m' }, { label: 'Year', value: '1y' }]} value={range} onChange={setRange} />\n        <Card style={{ marginTop: 12 }}>\n          <View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 8 }}><Text style={[T.title1, T.num, { color: t.text }]}>132 kg</Text><Text style={[T.subhead, { color: t.success, fontWeight: '600' }]}>Deadlift +7 kg</Text></View>\n          <Spark values={[112, 114, 115, 118, 118, 121, 124, 125, 128, 130, 132]} height={100} style={{ marginTop: 8 }} />\n        </Card>\n        <SectionTitle title=\"Personal records\" />\n        <Group>\n          <Row icon=\"Trophy\" iconColor=\"#E8B04B\" iconBg={alpha('#E8B04B', 0.2)} title=\"Deadlift\" subtitle=\"Sat 11 May\" value=\"130 kg × 3\" />\n          <Row icon=\"Trophy\" iconColor=\"#E8B04B\" iconBg={alpha('#E8B04B', 0.2)} title=\"Barbell squat\" subtitle=\"Tue 7 May\" value=\"95 kg × 5\" />\n          <Row icon=\"Trophy\" iconColor=\"#E8B04B\" iconBg={alpha('#E8B04B', 0.2)} title=\"Pull-ups\" subtitle=\"Thu 2 May\" value=\"14 reps\" />\n        </Group>\n      </View>\n    </Scroll>\n  );\n}\n\nfunction PlansScreen() {\n  const t = useT();\n  const plans = [['Strength A', 'Mon · Thu', '#F43F5E', 'Squat, bench, deadlift'], ['Strength B', 'Tue · Fri', '#A78BFA', 'Press, rows, hinge'], ['Conditioning', 'Sat', '#38BDF8', 'Intervals and carries'], ['Mobility', 'Sun', '#22C55E', '25 minutes, easy']];\n  return (\n    <Scroll pad={0}>\n      <Header kicker=\"12 week block\" title=\"Plans\" right={<Button label=\"New\" icon=\"Plus\" small onPress={() => {}} />} />\n      <View style={{ paddingHorizontal: 20, gap: 12 }}>\n        {plans.map((p) => (\n          <Card key={p[0]} onPress={() => {}}>\n            <View style={{ flexDirection: 'row', alignItems: 'center', gap: 14 }}>\n              <View style={{ width: 48, height: 48, borderRadius: 16, backgroundColor: alpha(p[2], 0.2), alignItems: 'center', justifyContent: 'center' }}><Icon name=\"Dumbbell\" size={24} color={p[2]} /></View>\n              <View style={{ flex: 1 }}><Text style={[T.headline, { color: t.text }]}>{p[0]}</Text><Text style={[T.footnote, { color: t.text2 }]}>{p[1]} · {p[3]}</Text></View>\n              <Icon name=\"ChevronRight\" size={18} color={t.text3} />\n            </View>\n          </Card>\n        ))}\n        <SectionTitle title=\"Block progress\" style={{ marginTop: 6 }} />\n        <Card><View style={{ flexDirection: 'row', justifyContent: 'space-between' }}><Text style={[T.subhead, { color: t.text2 }]}>Week 6 of 12</Text><Text style={[T.subhead, T.num, { color: t.text }]}>50%</Text></View><Progress value={0.5} style={{ marginTop: 10 }} /></Card>\n      </View>\n    </Scroll>\n  );\n}\n\nfunction ExerciseDetail({ nav, params }) {\n  const t = useT();\n  const { done, logSet } = useContext(Store);\n  const e = PLAN.find((x) => x.id === params.id) || PLAN[0];\n  const d = done[e.id] || 0;\n  const [rest, setRest] = useState(0);\n  useEffect(() => { if (rest <= 0) return; const id = setInterval(() => setRest((r) => Math.max(0, r - 1)), 1000); return () => clearInterval(id); }, [rest > 0]);\n  return (\n    <Screen>\n      <NavBar title={e.name} onBack={nav.pop} right={<Icon name=\"Info\" size={20} />} />\n      <Scroll pad={20}>\n        <View style={{ alignItems: 'center', marginTop: 6 }}>\n          <Ring progress={d / e.sets} size={170} stroke={16} color={e.c}>\n            <Text style={[T.largeTitle, T.num, { color: t.text }]}>{d}<Text style={[T.title3, { color: t.text2 }]}>/{e.sets}</Text></Text>\n            <Text style={[T.caption, { color: t.text2 }]}>sets done</Text>\n          </Ring>\n        </View>\n        <View style={{ flexDirection: 'row', gap: 12, marginTop: 20 }}>\n          <Card style={{ flex: 1 }}><Stat label=\"Target\" value={e.reps + (e.id === 5 ? ' s' : ' reps')} /></Card>\n          <Card style={{ flex: 1 }}><Stat label=\"Weight\" value={e.kg ? e.kg + ' kg' : 'Body'} /></Card>\n          <Card style={{ flex: 1 }}><Stat label=\"Rest\" value={rest ? rest + ' s' : '90 s'} color={rest ? e.c : t.text} /></Card>\n        </View>\n        <Spacer h={14} />\n        <Button label={d >= e.sets ? 'All sets done' : 'Log set ' + (d + 1)} icon={d >= e.sets ? 'Check' : 'Plus'} color={e.c} full disabled={d >= e.sets} onPress={() => { logSet(e.id); setRest(90); }} />\n        <SectionTitle title=\"Sets\" />\n        <Group>\n          {Array.from({ length: e.sets }).map((_, i) => <Row key={i} leading={<Checkbox checked={i < d} color={e.c} onChange={() => {}} />} title={'Set ' + (i + 1)} subtitle={i < d ? e.reps + ' × ' + (e.kg || 'BW') + ' · felt 7/10' : 'Up next'} value={i < d ? '✓' : ''} />)}\n        </Group>\n        <SectionTitle title=\"Last time\" />\n        <Card><Text style={[T.subhead, { color: t.text2 }]}>Thu 9 May</Text><Text style={[T.body, { color: t.text, marginTop: 4 }]}>{e.sets} × {e.reps} at {e.kg ? e.kg - 2.5 + ' kg' : 'bodyweight'}. Felt strong, bar speed good on the last set.</Text></Card>\n      </Scroll>\n    </Screen>\n  );\n}\n\nfunction Session({ nav }) {\n  const t = useT();\n  const { done } = useContext(Store);\n  const [sec, setSec] = useState(0);\n  useEffect(() => { const id = setInterval(() => setSec((s) => s + 1), 1000); return () => clearInterval(id); }, []);\n  const mm = Math.floor(sec / 60), ss = sec % 60;\n  return (\n    <Screen>\n      <NavBar title=\"Session\" onBack={nav.pop} right={<Pill label={mm + ':' + (ss < 10 ? '0' : '') + ss} color={ACCENT} />} />\n      <Scroll pad={20}>\n        <Card style={{ alignItems: 'center', paddingVertical: 26 }}>\n          <Text style={[T.caption, { color: t.text2, textTransform: 'uppercase', letterSpacing: 1 }]}>Elapsed</Text>\n          <Text style={[{ fontSize: 60, fontWeight: '700', letterSpacing: -2, color: t.text }, T.num]}>{mm}:{ss < 10 ? '0' + ss : ss}</Text>\n          <Text style={[T.subhead, { color: t.text2 }]}>{PLAN.filter((e) => done[e.id] >= e.sets).length} of {PLAN.length} exercises complete</Text>\n        </Card>\n        <SectionTitle title=\"Up next\" />\n        <Group>\n          {PLAN.map((e) => <Row key={e.id} icon={e.icon} iconColor={e.c} iconBg={alpha(e.c, 0.2)} title={e.name} subtitle={(done[e.id] || 0) + '/' + e.sets + ' sets'} chevron onPress={() => nav.push('exercise', { id: e.id })} />)}\n        </Group>\n        <Spacer h={16} />\n        <Button label=\"Finish workout\" icon=\"Flag\" full onPress={nav.pop} />\n      </Scroll>\n    </Screen>\n  );\n}\n\nfunction Root({ nav }) {\n  const [tab, setTab] = useState('today');\n  return (\n    <Screen>\n      <Tabs active={tab} screens={{ today: <TodayScreen nav={nav} />, progress: <ProgressScreen />, plans: <PlansScreen /> }} />\n      <TabBar active={tab} onChange={setTab} tabs={[\n        { key: 'today', label: 'Today', icon: 'Dumbbell' },\n        { key: 'progress', label: 'Progress', icon: 'TrendingUp' },\n        { key: 'plans', label: 'Plans', icon: 'CalendarDays' },\n      ]} />\n    </Screen>\n  );\n}\nconst SCREENS = { root: Root, exercise: ExerciseDetail, session: Session };\n\nexport default function App() {\n  const [done, setDone] = useState({ 1: 4, 2: 2 });\n  const logSet = (id) => setDone((d) => Object.assign({}, d, { [id]: (d[id] || 0) + 1 }));\n  return (\n    <ThemeProvider accent={ACCENT} overrides={THEME}>\n      <Store.Provider value={{ done, logSet }}>\n        <Stack screens={SCREENS} initial=\"root\" />\n      </Store.Provider>\n    </ThemeProvider>\n  );\n}\n","language":"react-native"},{"path":"ui/kit.tsx","type":"registry:component","rawUrl":"https://api.vp0.com/designs/forge-workout/raw?language=react-native&path=ui%2Fkit.tsx","target":"ui/kit.tsx","content":"import React, { useState, useEffect, useRef, useMemo, createContext, useContext } from 'react';\nimport { View, Text, Pressable, ScrollView, StyleSheet, Animated, Easing, Modal, Dimensions, useColorScheme, TextInput } from 'react-native';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\nimport Svg, { Circle, Path, Rect, Line, Polyline, Defs, LinearGradient as SvgGradient, Stop } from 'react-native-svg';\nimport * as Lucide from 'lucide-react-native';\nimport { LinearGradient } from 'expo-linear-gradient';\n\n// ─── colour helpers ─────────────────────────────────────────────────────────\nexport function alpha(hex, a) {\n  const h = hex.replace('#', '');\n  const n = parseInt(h.length === 3 ? h.split('').map((c) => c + c).join('') : h, 16);\n  const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;\n  return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';\n}\nexport function mix(hexA, hexB, p) {\n  const pa = parseInt(hexA.replace('#', ''), 16), pb = parseInt(hexB.replace('#', ''), 16);\n  const ch = (sh) => Math.round(((pa >> sh) & 255) * (1 - p) + ((pb >> sh) & 255) * p);\n  const to = (v) => ('0' + v.toString(16)).slice(-2);\n  return '#' + to(ch(16)) + to(ch(8)) + to(ch(0));\n}\nfunction luminance(hex) {\n  const n = parseInt(hex.replace('#', ''), 16);\n  const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;\n  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;\n}\n\n// ─── theme ──────────────────────────────────────────────────────────────────\nconst DARK = { bg: '#000000', card: '#1C1C1E', card2: '#2C2C2E', text: '#F2F2F7', text2: '#9A9AA3', text3: '#636369', line: '#2A2A2E' };\nconst LIGHT = { bg: '#F2F2F7', card: '#FFFFFF', card2: '#EDEDF2', text: '#111114', text2: '#6E6E76', text3: '#A6A6AE', line: '#E3E3E8' };\nconst SEMANTIC = { success: '#30D158', warning: '#FFB340', danger: '#FF453A', info: '#4DA3FF' };\nconst ThemeCtx = createContext(null);\n\nexport function makeTheme(accent, isDark, overrides) {\n  const base = isDark ? DARK : LIGHT;\n  const o = (overrides && (isDark ? overrides.dark : overrides.light)) || {};\n  const t = Object.assign({}, base, SEMANTIC, o, { accent: accent, isDark: isDark });\n  t.onAccent = luminance(accent) > 0.6 ? '#0B0B10' : '#FFFFFF';\n  t.accentSoft = alpha(accent, isDark ? 0.22 : 0.14);\n  t.accentFaint = alpha(accent, isDark ? 0.12 : 0.08);\n  return t;\n}\nexport function ThemeProvider({ accent, overrides, children }) {\n  const scheme = useColorScheme();\n  const t = useMemo(() => makeTheme(accent, scheme !== 'light', overrides), [accent, scheme, overrides]);\n  return <ThemeCtx.Provider value={t}>{children}</ThemeCtx.Provider>;\n}\nexport function useT() {\n  const t = useContext(ThemeCtx);\n  return t || makeTheme('#7C5CFF', true, null);\n}\nexport const useInsets = useSafeAreaInsets;\nexport const win = () => Dimensions.get('window');\n\n// ─── type scale (iOS) ───────────────────────────────────────────────────────\nexport const T = StyleSheet.create({\n  largeTitle: { fontSize: 34, fontWeight: '700', letterSpacing: -0.7, lineHeight: 40 },\n  title1: { fontSize: 28, fontWeight: '700', letterSpacing: -0.5, lineHeight: 34 },\n  title2: { fontSize: 22, fontWeight: '700', letterSpacing: -0.35, lineHeight: 28 },\n  title3: { fontSize: 20, fontWeight: '600', letterSpacing: -0.25, lineHeight: 25 },\n  headline: { fontSize: 17, fontWeight: '600', letterSpacing: -0.2, lineHeight: 22 },\n  body: { fontSize: 17, fontWeight: '400', letterSpacing: -0.2, lineHeight: 22 },\n  callout: { fontSize: 16, fontWeight: '400', lineHeight: 21 },\n  subhead: { fontSize: 15, fontWeight: '400', lineHeight: 20 },\n  footnote: { fontSize: 13, fontWeight: '400', lineHeight: 18 },\n  caption: { fontSize: 12, fontWeight: '500', lineHeight: 16 },\n  caption2: { fontSize: 11, fontWeight: '600', lineHeight: 13, letterSpacing: 0.3 },\n  num: { fontVariant: ['tabular-nums'] },\n});\n\n// ─── motion ─────────────────────────────────────────────────────────────────\nexport function useTween(target, ms) {\n  const dur = ms || 650;\n  const [v, setV] = useState(target);\n  const ref = useRef({ raf: 0 });\n  useEffect(() => {\n    const from = v, to = target, t0 = Date.now();\n    if (from === to) return;\n    cancelAnimationFrame(ref.current.raf);\n    const tick = () => {\n      const p = Math.min(1, (Date.now() - t0) / dur);\n      const e = 1 - Math.pow(1 - p, 3);\n      setV(from + (to - from) * e);\n      if (p < 1) ref.current.raf = requestAnimationFrame(tick);\n    };\n    ref.current.raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(ref.current.raf);\n  }, [target]);\n  return v;\n}\nexport function usePressScale(to) {\n  const scale = useRef(new Animated.Value(1)).current;\n  const onPressIn = () => Animated.spring(scale, { toValue: to || 0.96, useNativeDriver: false, speed: 40, bounciness: 2 }).start();\n  const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: false, speed: 20, bounciness: 8 }).start();\n  return { scale, onPressIn, onPressOut };\n}\n\n// ─── icons ──────────────────────────────────────────────────────────────────\nexport function Icon({ name, size, color, strokeWidth, fill, style }) {\n  const t = useT();\n  const Cmp = Lucide[name] || Lucide.Circle;\n  return <Cmp size={size || 22} color={color || t.text} strokeWidth={strokeWidth || 2} fill={fill || 'none'} style={style} />;\n}\n\n// ─── layout primitives ──────────────────────────────────────────────────────\nexport function Screen({ children, style, bg }) {\n  const t = useT();\n  return <View style={[{ flex: 1, backgroundColor: bg || t.bg }, style]}>{children}</View>;\n}\nexport function Scroll({ children, style, contentStyle, pad, horizontal, bottom }) {\n  const p = pad == null ? 20 : pad;\n  return (\n    <ScrollView\n      horizontal={!!horizontal}\n      style={[{ flex: 1 }, style]}\n      showsVerticalScrollIndicator={false}\n      showsHorizontalScrollIndicator={false}\n      contentContainerStyle={[horizontal ? { paddingHorizontal: p, gap: 12 } : { paddingHorizontal: p, paddingBottom: bottom == null ? 28 : bottom }, contentStyle]}\n    >\n      {children}\n    </ScrollView>\n  );\n}\nexport function Spacer({ h, w }) { return <View style={{ height: h || 0, width: w || 0 }} />; }\nexport function Divider({ inset, style }) {\n  const t = useT();\n  return <View style={[{ height: StyleSheet.hairlineWidth, backgroundColor: t.line, marginLeft: inset || 0 }, style]} />;\n}\n\n/** Large-title header for a tab/root screen: kicker, 34pt title, optional trailing slot. */\nexport function Header({ kicker, title, subtitle, right, style, kickerColor }) {\n  const t = useT();\n  const ins = useInsets();\n  return (\n    <View style={[{ paddingTop: ins.top + 6, paddingHorizontal: 20, paddingBottom: 12, flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between' }, style]}>\n      <View style={{ flex: 1, minWidth: 0 }}>\n        {kicker ? <Text style={[T.caption2, { color: kickerColor || t.accent, textTransform: 'uppercase', letterSpacing: 1.2, marginBottom: 4 }]}>{kicker}</Text> : null}\n        <Text style={[T.largeTitle, { color: t.text }]} numberOfLines={1}>{title}</Text>\n        {subtitle ? <Text style={[T.subhead, { color: t.text2, marginTop: 3 }]}>{subtitle}</Text> : null}\n      </View>\n      {right ? <View style={{ marginLeft: 12, marginBottom: 4 }}>{right}</View> : null}\n    </View>\n  );\n}\n\n/** Inline navigation bar for a pushed screen: back chevron, centred title, trailing slot. */\nexport function NavBar({ title, onBack, right, transparent, tint }) {\n  const t = useT();\n  const ins = useInsets();\n  const color = tint || t.text;\n  return (\n    <View style={{ paddingTop: ins.top + 2, height: ins.top + 46, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, backgroundColor: transparent ? 'transparent' : t.bg }}>\n      <Pressable onPress={onBack} hitSlop={10} style={({ pressed }) => [{ width: 44, height: 40, alignItems: 'center', justifyContent: 'center', borderRadius: 20 }, pressed && { opacity: 0.5 }]}>\n        <Icon name=\"ChevronLeft\" size={26} color={color} strokeWidth={2.4} />\n      </Pressable>\n      <Text style={[T.headline, { color: color, flex: 1, textAlign: 'center' }]} numberOfLines={1}>{title}</Text>\n      <View style={{ width: 44, alignItems: 'flex-end' }}>{right || null}</View>\n    </View>\n  );\n}\n\n/** Section label (iOS grouped header) with optional trailing action. */\nexport function SectionTitle({ title, action, onAction, style }) {\n  const t = useT();\n  return (\n    <View style={[{ flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between', marginTop: 22, marginBottom: 10 }, style]}>\n      <Text style={[T.title3, { color: t.text }]}>{title}</Text>\n      {action ? (\n        <Pressable onPress={onAction} hitSlop={8}>\n          <Text style={[T.subhead, { color: t.accent, fontWeight: '600' }]}>{action}</Text>\n        </Pressable>\n      ) : null}\n    </View>\n  );\n}\n\nexport function Card({ children, style, onPress, pad, bg }) {\n  const t = useT();\n  const base = [{ backgroundColor: bg || t.card, borderRadius: 20, padding: pad == null ? 16 : pad }, style];\n  if (!onPress) return <View style={base}>{children}</View>;\n  return (\n    <Pressable onPress={onPress} style={({ pressed }) => [base, pressed && { opacity: 0.86, transform: [{ scale: 0.985 }] }]}>\n      {children}\n    </Pressable>\n  );\n}\n\n/** Inset-grouped list: wraps rows in a card, drawing hairline separators. */\nexport function Group({ children, style }) {\n  const t = useT();\n  const kids = React.Children.toArray(children).filter(Boolean);\n  return (\n    <View style={[{ backgroundColor: t.card, borderRadius: 20, overflow: 'hidden' }, style]}>\n      {kids.map((k, i) => (\n        <View key={i}>\n          {k}\n          {i < kids.length - 1 ? <Divider inset={60} /> : null}\n        </View>\n      ))}\n    </View>\n  );\n}\n\n/** A list row: leading icon tile (lucide or emoji/custom), title, subtitle, trailing value, chevron. */\nexport function Row({ icon, emoji, iconColor, iconBg, leading, title, subtitle, value, valueColor, sub2, chevron, onPress, right, pad, titleStyle }) {\n  const t = useT();\n  const body = (\n    <View style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: pad == null ? 12 : pad, paddingHorizontal: 14 }}>\n      {leading ? leading : (icon || emoji) ? (\n        <View style={{ width: 40, height: 40, borderRadius: 12, backgroundColor: iconBg || t.accentSoft, alignItems: 'center', justifyContent: 'center' }}>\n          {emoji ? <Text style={{ fontSize: 20 }}>{emoji}</Text> : <Icon name={icon} size={20} color={iconColor || t.accent} strokeWidth={2.2} />}\n        </View>\n      ) : null}\n      <View style={{ flex: 1, minWidth: 0, marginLeft: (leading || icon || emoji) ? 12 : 0 }}>\n        <Text style={[T.body, { color: t.text, fontWeight: '500' }, titleStyle]} numberOfLines={1}>{title}</Text>\n        {subtitle ? <Text style={[T.footnote, { color: t.text2, marginTop: 1 }]} numberOfLines={1}>{subtitle}</Text> : null}\n      </View>\n      {value != null ? (\n        <View style={{ alignItems: 'flex-end', marginLeft: 10 }}>\n          <Text style={[T.body, T.num, { color: valueColor || t.text, fontWeight: '600' }]}>{value}</Text>\n          {sub2 ? <Text style={[T.caption, { color: t.text3 }]}>{sub2}</Text> : null}\n        </View>\n      ) : null}\n      {right ? <View style={{ marginLeft: 10 }}>{right}</View> : null}\n      {chevron ? <Icon name=\"ChevronRight\" size={18} color={t.text3} style={{ marginLeft: 6 }} /> : null}\n    </View>\n  );\n  if (!onPress) return body;\n  return <Pressable onPress={onPress} style={({ pressed }) => [pressed && { backgroundColor: t.card2 }]}>{body}</Pressable>;\n}\n\n// ─── controls ───────────────────────────────────────────────────────────────\nexport function Button({ label, onPress, kind, icon, small, full, color, style, disabled }) {\n  const t = useT();\n  const { scale, onPressIn, onPressOut } = usePressScale(0.96);\n  const k = kind || 'primary';\n  const accent = color || t.accent;\n  const bg = k === 'primary' ? accent : k === 'secondary' ? t.card2 : k === 'soft' ? alpha(accent, t.isDark ? 0.22 : 0.14) : 'transparent';\n  const fg = k === 'primary' ? (luminance(accent) > 0.6 ? '#0B0B10' : '#FFFFFF') : k === 'secondary' ? t.text : accent;\n  return (\n    <Animated.View style={[{ transform: [{ scale }] }, full && { alignSelf: 'stretch' }, style]}>\n      <Pressable\n        onPress={onPress}\n        onPressIn={onPressIn}\n        onPressOut={onPressOut}\n        disabled={disabled}\n        style={{ backgroundColor: bg, borderRadius: small ? 14 : 18, paddingVertical: small ? 9 : 15, paddingHorizontal: small ? 14 : 20, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, opacity: disabled ? 0.5 : 1 }}\n      >\n        {icon ? <Icon name={icon} size={small ? 16 : 18} color={fg} strokeWidth={2.4} /> : null}\n        <Text style={[small ? T.subhead : T.headline, { color: fg, fontWeight: '600' }]}>{label}</Text>\n      </Pressable>\n    </Animated.View>\n  );\n}\n\nexport function IconButton({ name, onPress, size, bg, color, style }) {\n  const t = useT();\n  const s = size || 40;\n  return (\n    <Pressable onPress={onPress} hitSlop={6} style={({ pressed }) => [{ width: s, height: s, borderRadius: s / 2, backgroundColor: bg || t.card2, alignItems: 'center', justifyContent: 'center' }, pressed && { opacity: 0.6 }, style]}>\n      <Icon name={name} size={Math.round(s * 0.5)} color={color || t.text} strokeWidth={2.2} />\n    </Pressable>\n  );\n}\n\nexport function Chip({ label, active, onPress, icon, color, style }) {\n  const t = useT();\n  const c = color || t.accent;\n  return (\n    <Pressable onPress={onPress} style={({ pressed }) => [{ paddingHorizontal: 14, paddingVertical: 8, borderRadius: 999, backgroundColor: active ? c : t.card, flexDirection: 'row', alignItems: 'center', gap: 6 }, pressed && { opacity: 0.7 }, style]}>\n      {icon ? <Icon name={icon} size={14} color={active ? (luminance(c) > 0.6 ? '#0B0B10' : '#fff') : t.text2} strokeWidth={2.4} /> : null}\n      <Text style={[T.subhead, { color: active ? (luminance(c) > 0.6 ? '#0B0B10' : '#fff') : t.text, fontWeight: '600' }]}>{label}</Text>\n    </Pressable>\n  );\n}\n\nexport function Pill({ label, color, style, small }) {\n  const t = useT();\n  const c = color || t.accent;\n  return (\n    <View style={[{ alignSelf: 'flex-start', paddingHorizontal: small ? 7 : 9, paddingVertical: small ? 2 : 4, borderRadius: 8, backgroundColor: alpha(c, t.isDark ? 0.24 : 0.14) }, style]}>\n      <Text style={[small ? T.caption2 : T.caption, { color: t.isDark ? mix(c, '#FFFFFF', 0.25) : mix(c, '#000000', 0.2), fontWeight: '700' }]}>{label}</Text>\n    </View>\n  );\n}\n\n/** iOS segmented control with a sliding thumb. */\nexport function Segmented({ options, value, onChange, style }) {\n  const t = useT();\n  const idx = Math.max(0, options.findIndex((o) => (o.value == null ? o : o.value) === value));\n  const x = useRef(new Animated.Value(idx)).current;\n  const [w, setW] = useState(0);\n  useEffect(() => { Animated.spring(x, { toValue: idx, useNativeDriver: false, speed: 24, bounciness: 4 }).start(); }, [idx]);\n  const segW = options.length ? (w - 4) / options.length : 0;\n  return (\n    <View onLayout={(e) => setW(e.nativeEvent.layout.width)} style={[{ height: 36, borderRadius: 11, backgroundColor: t.isDark ? t.card2 : '#E2E2E8', padding: 2, flexDirection: 'row' }, style]}>\n      {w > 0 ? (\n        <Animated.View style={{ position: 'absolute', top: 2, left: 2, width: segW, height: 32, borderRadius: 9, backgroundColor: t.isDark ? '#5A5A5F' : '#FFFFFF', transform: [{ translateX: Animated.multiply(x, segW) }], shadowColor: '#000', shadowOpacity: t.isDark ? 0 : 0.12, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } }} />\n      ) : null}\n      {options.map((o, i) => {\n        const v = o.value == null ? o : o.value;\n        const label = o.label == null ? o : o.label;\n        return (\n          <Pressable key={String(v)} onPress={() => onChange(v)} style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n            <Text style={[T.footnote, { color: t.text, fontWeight: i === idx ? '600' : '500' }]}>{label}</Text>\n          </Pressable>\n        );\n      })}\n    </View>\n  );\n}\n\n/** iOS toggle (own implementation so both appearances look native). */\nexport function Toggle({ value, onChange, color }) {\n  const t = useT();\n  const x = useRef(new Animated.Value(value ? 1 : 0)).current;\n  useEffect(() => { Animated.spring(x, { toValue: value ? 1 : 0, useNativeDriver: false, speed: 30, bounciness: 3 }).start(); }, [value]);\n  const bg = x.interpolate({ inputRange: [0, 1], outputRange: [t.isDark ? '#39393D' : '#E9E9EB', color || t.success] });\n  return (\n    <Pressable onPress={() => onChange(!value)} hitSlop={8}>\n      <Animated.View style={{ width: 51, height: 31, borderRadius: 16, backgroundColor: bg, padding: 2 }}>\n        <Animated.View style={{ width: 27, height: 27, borderRadius: 14, backgroundColor: '#fff', transform: [{ translateX: x.interpolate({ inputRange: [0, 1], outputRange: [0, 20] }) }], shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 3, shadowOffset: { width: 0, height: 2 } }} />\n      </Animated.View>\n    </Pressable>\n  );\n}\n\nexport function Checkbox({ checked, onChange, color, size }) {\n  const t = useT();\n  const s = size || 26;\n  const c = color || t.accent;\n  return (\n    <Pressable onPress={() => onChange && onChange(!checked)} hitSlop={8} style={{ width: s, height: s, borderRadius: s / 2, borderWidth: 2, borderColor: checked ? c : t.text3, backgroundColor: checked ? c : 'transparent', alignItems: 'center', justifyContent: 'center' }}>\n      {checked ? <Icon name=\"Check\" size={s * 0.6} color={luminance(c) > 0.6 ? '#0B0B10' : '#fff'} strokeWidth={3.2} /> : null}\n    </Pressable>\n  );\n}\n\nexport function SearchBar({ placeholder, value, onChange, style }) {\n  const t = useT();\n  return (\n    <View style={[{ height: 40, borderRadius: 12, backgroundColor: t.isDark ? t.card : '#E4E4E9', flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, gap: 8 }, style]}>\n      <Icon name=\"Search\" size={18} color={t.text3} strokeWidth={2.2} />\n      <TextInput value={value} onChangeText={onChange} placeholder={placeholder || 'Search'} placeholderTextColor={t.text3} style={[T.body, { flex: 1, color: t.text, paddingVertical: 0 }]} />\n    </View>\n  );\n}\n\n// ─── data display ───────────────────────────────────────────────────────────\nexport function Stat({ label, value, delta, up, style, color, big }) {\n  const t = useT();\n  return (\n    <View style={[{ flex: 1 }, style]}>\n      <Text style={[T.caption, { color: t.text2 }]}>{label}</Text>\n      <Text style={[big ? T.title1 : T.title2, T.num, { color: color || t.text, marginTop: 2 }]}>{value}</Text>\n      {delta ? <Text style={[T.caption, { color: up === false ? t.danger : t.success, marginTop: 2, fontWeight: '600' }]}>{delta}</Text> : null}\n    </View>\n  );\n}\n\nexport function Avatar({ name, size, color, style, uri }) {\n  const t = useT();\n  const s = size || 40;\n  const c = color || t.accent;\n  const initials = (name || '?').split(' ').map((p) => p[0]).join('').slice(0, 2).toUpperCase();\n  return (\n    <View style={[{ width: s, height: s, borderRadius: s / 2, backgroundColor: alpha(c, 0.25), alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }, style]}>\n      <View style={{ position: 'absolute', left: -s * 0.2, top: -s * 0.2, width: s * 0.9, height: s * 0.9, borderRadius: s, backgroundColor: alpha(c, 0.35) }} />\n      <Text style={{ color: t.isDark ? mix(c, '#FFFFFF', 0.45) : mix(c, '#000000', 0.25), fontSize: s * 0.38, fontWeight: '700' }}>{initials}</Text>\n    </View>\n  );\n}\n\nexport function Progress({ value, color, height, track, style }) {\n  const t = useT();\n  const v = useTween(Math.max(0, Math.min(1, value)));\n  const h = height || 8;\n  return (\n    <View style={[{ height: h, borderRadius: h / 2, backgroundColor: track || t.card2, overflow: 'hidden' }, style]}>\n      <View style={{ width: (v * 100) + '%', height: h, borderRadius: h / 2, backgroundColor: color || t.accent }} />\n    </View>\n  );\n}\n\n/** Circular progress ring (SVG). progress 0..1. Children render in the middle. */\nexport function Ring({ progress, size, stroke, color, track, children, style }) {\n  const t = useT();\n  const s = size || 160;\n  const sw = stroke || 14;\n  const r = (s - sw) / 2;\n  const c = 2 * Math.PI * r;\n  const p = useTween(Math.max(0, Math.min(1, progress)));\n  return (\n    <View style={[{ width: s, height: s, alignItems: 'center', justifyContent: 'center' }, style]}>\n      <Svg width={s} height={s} style={{ position: 'absolute', transform: [{ rotate: '-90deg' }] }}>\n        <Circle cx={s / 2} cy={s / 2} r={r} stroke={track || t.card2} strokeWidth={sw} fill=\"none\" />\n        <Circle cx={s / 2} cy={s / 2} r={r} stroke={color || t.accent} strokeWidth={sw} fill=\"none\" strokeLinecap=\"round\" strokeDasharray={c + ' ' + c} strokeDashoffset={c * (1 - p)} />\n      </Svg>\n      {children}\n    </View>\n  );\n}\n\n/** Bar chart (SVG). values: numbers; labels optional; highlight index. */\nexport function Bars({ values, labels, height, color, dim, highlight, gap, radius, style, max }) {\n  const t = useT();\n  const h = height || 120;\n  const [w, setW] = useState(0);\n  const m = max || Math.max(1, ...values);\n  const n = values.length;\n  const g = gap == null ? 8 : gap;\n  const bw = n ? (w - g * (n - 1)) / n : 0;\n  const rad = radius == null ? Math.min(6, bw / 2) : radius;\n  const lh = labels ? 18 : 0;\n  return (\n    <View onLayout={(e) => setW(e.nativeEvent.layout.width)} style={[{ height: h + lh }, style]}>\n      {w > 0 ? (\n        <Svg width={w} height={h}>\n          {values.map((v, i) => {\n            const bh = Math.max(4, (v / m) * h);\n            const on = highlight == null || highlight === i;\n            return <Rect key={i} x={i * (bw + g)} y={h - bh} width={bw} height={bh} rx={rad} fill={on ? (color || t.accent) : (dim || t.card2)} />;\n          })}\n        </Svg>\n      ) : null}\n      {labels ? (\n        <View style={{ flexDirection: 'row', marginTop: 4 }}>\n          {labels.map((l, i) => <Text key={i} style={[T.caption2, { color: highlight === i ? t.text : t.text3, flex: 1, textAlign: 'center' }]}>{l}</Text>)}\n        </View>\n      ) : null}\n    </View>\n  );\n}\n\n/** Sparkline / area line (SVG). */\nexport function Spark({ values, height, color, fill, stroke, style, dot }) {\n  const t = useT();\n  const h = height || 80;\n  const [w, setW] = useState(0);\n  const c = color || t.accent;\n  const min = Math.min(...values), max = Math.max(...values);\n  const span = max - min || 1;\n  const pts = values.map((v, i) => [values.length > 1 ? (i / (values.length - 1)) * w : 0, 6 + (1 - (v - min) / span) * (h - 12)]);\n  const d = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');\n  const area = d + ' L' + w + ' ' + h + ' L0 ' + h + ' Z';\n  const last = pts[pts.length - 1] || [0, 0];\n  return (\n    <View onLayout={(e) => setW(e.nativeEvent.layout.width)} style={[{ height: h }, style]}>\n      {w > 0 ? (\n        <Svg width={w} height={h}>\n          <Defs>\n            <SvgGradient id=\"sparkfill\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <Stop offset=\"0\" stopColor={c} stopOpacity={t.isDark ? 0.35 : 0.25} />\n              <Stop offset=\"1\" stopColor={c} stopOpacity=\"0\" />\n            </SvgGradient>\n          </Defs>\n          {fill !== false ? <Path d={area} fill=\"url(#sparkfill)\" /> : null}\n          <Path d={d} stroke={c} strokeWidth={stroke || 2.5} fill=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n          {dot !== false ? <Circle cx={last[0]} cy={last[1]} r={4} fill={c} stroke={t.card} strokeWidth={2} /> : null}\n        </Svg>\n      ) : null}\n    </View>\n  );\n}\n\n// ─── navigation ─────────────────────────────────────────────────────────────\n/** Bottom tab bar. tabs: [{ key, label, icon }]. */\nexport function TabBar({ tabs, active, onChange, style, accent }) {\n  const t = useT();\n  const ins = useInsets();\n  const c = accent || t.accent;\n  return (\n    <View style={[{ paddingBottom: Math.max(ins.bottom, 10), paddingTop: 8, backgroundColor: t.isDark ? alpha(t.card, 0.96) : alpha('#FFFFFF', 0.96), borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: t.line, flexDirection: 'row' }, style]}>\n      {tabs.map((tab) => {\n        const on = tab.key === active;\n        return (\n          <Pressable key={tab.key} onPress={() => onChange(tab.key)} style={({ pressed }) => [{ flex: 1, alignItems: 'center', gap: 3, paddingVertical: 2 }, pressed && { opacity: 0.6 }]}>\n            <Icon name={tab.icon} size={24} color={on ? c : t.text3} strokeWidth={on ? 2.4 : 2} fill={on && tab.fillOnActive ? c : 'none'} />\n            <Text style={[{ fontSize: 10, fontWeight: on ? '600' : '500', color: on ? c : t.text3 }]}>{tab.label}</Text>\n          </Pressable>\n        );\n      })}\n    </View>\n  );\n}\n\n/** Tab screens: every tab stays mounted (state + scroll position survive a switch); only the active one is displayed. */\nexport function Tabs({ active, screens }) {\n  return (\n    <View style={{ flex: 1 }}>\n      {Object.keys(screens).map((k) => (\n        <View key={k} style={{ flex: 1, display: k === active ? 'flex' : 'none' }}>{screens[k]}</View>\n      ))}\n    </View>\n  );\n}\n\n/**\n * Minimal navigation stack: screens = { name: Component }, each rendered with\n * { nav: { push, pop, canPop }, params }. Pushed screens slide in from the right\n * while the screen beneath parallaxes left (iOS push transition).\n */\nexport function Stack({ screens, initial, params }) {\n  const t = useT();\n  const [stack, setStack] = useState([{ key: 'root', name: initial, params: params || {} }]);\n  const [leavingKey, setLeavingKey] = useState(null);\n  const nav = useMemo(() => ({\n    push: (name, p) => setStack((s) => [...s, { key: name + '-' + Date.now(), name: name, params: p || {} }]),\n    pop: () => setStack((s) => { if (s.length > 1) setLeavingKey(s[s.length - 1].key); return s; }),\n    reset: (name, p) => setStack([{ key: 'root-' + Date.now(), name: name, params: p || {} }]),\n    canPop: stack.length > 1,\n  }), [stack.length]);\n  const width = win().width;\n  const finishPop = (key) => { setStack((s) => s.filter((e) => e.key !== key)); setLeavingKey(null); };\n  return (\n    <View style={{ flex: 1, overflow: 'hidden', backgroundColor: t.bg }}>\n      {stack.map((entry, i) => {\n        const Cmp = screens[entry.name];\n        const isTop = i === stack.length - 1;\n        const under = i === stack.length - 2 && stack.length > 1;\n        return (\n          <StackLayer key={entry.key} width={width} index={i} isTop={isTop} under={under} leaving={entry.key === leavingKey} onLeft={() => finishPop(entry.key)}>\n            {Cmp ? <Cmp nav={nav} params={entry.params} /> : null}\n          </StackLayer>\n        );\n      })}\n    </View>\n  );\n}\nfunction StackLayer({ children, width, index, isTop, under, leaving, onLeft }) {\n  const prog = useRef(new Animated.Value(index === 0 ? 1 : 0)).current;\n  const t = useT();\n  useEffect(() => {\n    if (index === 0) return;\n    Animated.timing(prog, { toValue: 1, duration: 320, easing: Easing.out(Easing.cubic), useNativeDriver: false }).start();\n  }, []);\n  useEffect(() => {\n    if (!leaving) return;\n    Animated.timing(prog, { toValue: 0, duration: 280, easing: Easing.inOut(Easing.cubic), useNativeDriver: false }).start(() => onLeft());\n  }, [leaving]);\n  const tx = index === 0 ? 0 : prog.interpolate({ inputRange: [0, 1], outputRange: [width, 0] });\n  return (\n    <Animated.View pointerEvents={isTop ? 'auto' : 'none'} style={[StyleSheet.absoluteFill, { backgroundColor: t.bg, transform: [{ translateX: tx }], shadowColor: '#000', shadowOpacity: index === 0 ? 0 : 0.25, shadowRadius: 16, shadowOffset: { width: -4, height: 0 } }]}>\n      {children}\n      {under ? <View pointerEvents=\"none\" style={[StyleSheet.absoluteFill, { backgroundColor: alpha('#000000', t.isDark ? 0.35 : 0.12) }]} /> : null}\n    </Animated.View>\n  );\n}\n\n/** Bottom sheet over a dimmed backdrop. */\nexport function Sheet({ visible, onClose, title, children, height }) {\n  const t = useT();\n  const ins = useInsets();\n  const y = useRef(new Animated.Value(1)).current;\n  const [mounted, setMounted] = useState(visible);\n  useEffect(() => {\n    if (visible) { setMounted(true); Animated.spring(y, { toValue: 0, useNativeDriver: false, speed: 18, bounciness: 2 }).start(); }\n    else Animated.timing(y, { toValue: 1, duration: 220, easing: Easing.in(Easing.cubic), useNativeDriver: false }).start(() => setMounted(false));\n  }, [visible]);\n  if (!mounted) return null;\n  const h = height || 420;\n  return (\n    <Modal transparent visible animationType=\"none\" onRequestClose={onClose}>\n      <Pressable onPress={onClose} style={{ flex: 1, backgroundColor: alpha('#000000', 0.45) }} />\n      <Animated.View style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: h + ins.bottom, backgroundColor: t.isDark ? t.card : '#FFFFFF', borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingHorizontal: 20, paddingBottom: ins.bottom + 12, transform: [{ translateY: y.interpolate({ inputRange: [0, 1], outputRange: [0, h + 60] }) }] }}>\n        <View style={{ alignSelf: 'center', width: 36, height: 5, borderRadius: 3, backgroundColor: t.text3, opacity: 0.5, marginTop: 8, marginBottom: 12 }} />\n        {title ? <Text style={[T.title3, { color: t.text, marginBottom: 12 }]}>{title}</Text> : null}\n        {children}\n      </Animated.View>\n    </Modal>\n  );\n}\n\n/** Linear gradient surface (expo-linear-gradient, vendored). colors: 2+ hex. */\nexport function Gradient({ colors, style, children, start, end, angle }) {\n  const a = angle == null ? 135 : angle;\n  const rad = (a * Math.PI) / 180;\n  const s = start || { x: 0.5 - Math.sin(rad) / 2, y: 0.5 + Math.cos(rad) / 2 };\n  const e = end || { x: 0.5 + Math.sin(rad) / 2, y: 0.5 - Math.cos(rad) / 2 };\n  return <LinearGradient colors={colors} start={s} end={e} style={style}>{children}</LinearGradient>;\n}\n\n/** Soft radial-ish glow block for hero areas (two stacked translucent circles). */\nexport function Glow({ color, size, style, opacity }) {\n  const s = size || 260;\n  return (\n    <View pointerEvents=\"none\" style={[{ position: 'absolute', width: s, height: s, borderRadius: s / 2, backgroundColor: alpha(color, opacity == null ? 0.18 : opacity) }, style]}>\n      <View style={{ position: 'absolute', left: s * 0.2, top: s * 0.2, width: s * 0.6, height: s * 0.6, borderRadius: s, backgroundColor: alpha(color, (opacity == null ? 0.18 : opacity) * 0.9) }} />\n    </View>\n  );\n}\n","language":"react-native"},{"path":"WorkoutView.swift","type":"registry:page","rawUrl":"https://api.vp0.com/designs/forge-workout/raw?language=swiftui&path=WorkoutView.swift","target":"app/WorkoutView.swift","content":"import SwiftUI\n\nstruct Exercise: Identifiable { let id = UUID(); let name: String; let sets: String; let icon: String; var done = false }\n\nstruct WorkoutView: View {\n    @State private var exercises: [Exercise] = [\n        .init(name: \"Barbell squat\", sets: \"4 × 8\", icon: \"figure.strengthtraining.traditional\"),\n        .init(name: \"Bench press\", sets: \"4 × 10\", icon: \"dumbbell.fill\"),\n        .init(name: \"Deadlift\", sets: \"3 × 6\", icon: \"figure.strengthtraining.functional\"),\n        .init(name: \"Pull-ups\", sets: \"3 × 12\", icon: \"figure.climbing\"),\n        .init(name: \"Plank\", sets: \"3 × 60s\", icon: \"figure.core.training\")\n    ]\n    private var done: Int { exercises.filter(\\.done).count }\n\n    var body: some View {\n        ZStack {\n            Color(red: 0.05, green: 0.03, blue: 0.04).ignoresSafeArea()\n            ScrollView {\n                VStack(alignment: .leading, spacing: 16) {\n                    Text(\"PUSH DAY · 48 MIN\").font(.caption).bold().foregroundColor(Color(red: 0.96, green: 0.25, blue: 0.37))\n                    Text(\"Strength A\").font(.system(size: 34, weight: .heavy)).foregroundColor(.white)\n                    HStack {\n                        stat(\"\\(done)/\\(exercises.count)\", \"exercises\")\n                        stat(\"1,240\", \"kg lifted\")\n                        stat(\"320\", \"kcal\")\n                    }\n                    .padding(18).background(Color(red: 0.10, green: 0.05, blue: 0.07)).cornerRadius(18)\n\n                    ForEach($exercises) { $ex in\n                        Button { ex.done.toggle() } label: {\n                            HStack(spacing: 14) {\n                                Image(systemName: ex.icon).font(.title2).foregroundColor(.white).frame(width: 30)\n                                VStack(alignment: .leading, spacing: 3) {\n                                    Text(ex.name).fontWeight(.semibold).foregroundColor(.white)\n                                    Text(ex.sets).font(.footnote).foregroundColor(.gray)\n                                }\n                                Spacer()\n                                Image(systemName: ex.done ? \"checkmark.circle.fill\" : \"circle\")\n                                    .foregroundColor(ex.done ? Color(red: 0.96, green: 0.25, blue: 0.37) : .gray).font(.title2)\n                            }\n                            .padding(16).background(Color.white.opacity(0.04)).cornerRadius(16)\n                        }\n                    }\n                }\n                .padding(22)\n            }\n        }\n    }\n\n    private func stat(_ value: String, _ label: String) -> some View {\n        VStack(spacing: 3) {\n            Text(value).font(.title3).bold().foregroundColor(.white)\n            Text(label).font(.caption).foregroundColor(.gray)\n        }.frame(maxWidth: .infinity)\n    }\n}\n\n#Preview { WorkoutView() }\n","language":"swiftui"}],"notes":"This is an iOS app UI starter built with React Native. Paste the design link into your AI app builder (Claude Code, Rork, Lovable) and ask it to start from this design. Add your own backend/data logic. These are UI starters.","title":"Forge — Workout","assets":[{"url":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/ce08a54c-a4b7-41fd-afa6-021d51edc952/full.webp","kind":"cover","lqip":"CDgGCwZ4f3eHgoBgdm949oc=","width":1206,"height":2622,"variants":{"avif":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/ce08a54c-a4b7-41fd-afa6-021d51edc952/avif.avif","card":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/ce08a54c-a4b7-41fd-afa6-021d51edc952/card.webp","full":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/ce08a54c-a4b7-41fd-afa6-021d51edc952/full.webp","thumb":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/ce08a54c-a4b7-41fd-afa6-021d51edc952/thumb.webp","light_avif":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/56c1e5ae-484e-433d-82e8-45511cbf3533/avif.avif","light_card":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/56c1e5ae-484e-433d-82e8-45511cbf3533/card.webp","light_full":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/56c1e5ae-484e-433d-82e8-45511cbf3533/full.webp","light_thumb":"https://storage.vp0.com/vp0/covers/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/56c1e5ae-484e-433d-82e8-45511cbf3533/thumb.webp"}},{"alt":"(root)","url":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-0.png","kind":"screenshot","lqip":"CDgGCwZ4f3eHgoBgdm949oc=","width":1206,"height":2622},{"alt":"view-2","url":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-1.png","kind":"screenshot","lqip":"xgcGEwRpdgeHhY8qh4d/ePg=","width":1206,"height":2622},{"alt":"view-3","url":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-2.png","kind":"screenshot","lqip":"BhgCAwJthgeItLRUYEkEljY=","width":1206,"height":2622},{"alt":"view-4","url":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-3.png","kind":"screenshot","lqip":"BRgCAwCNfzmzc3iAkSof2vI=","width":1206,"height":2622},{"url":"https://storage.vp0.com/vp0/clips/87/32f1d9ff-f91d-4a20-b213-b2e963f1f384/clip.mp4","kind":"video","variants":{"mp4":"https://storage.vp0.com/vp0/clips/87/32f1d9ff-f91d-4a20-b213-b2e963f1f384/clip.mp4","webm":"https://storage.vp0.com/vp0/clips/87/32f1d9ff-f91d-4a20-b213-b2e963f1f384/clip.webm","poster":"https://storage.vp0.com/vp0/clips/87/32f1d9ff-f91d-4a20-b213-b2e963f1f384/poster.jpg"}},{"url":"https://storage.vp0.com/vp0/share/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/3769277c-3a9d-405b-9031-9ebcec84b911/share.mp4","kind":"share_video"}],"author":"@vp0","$schema":"https://api.vp0.com/schema/vp0-bundle.v1.json","license":null,"screens":[{"name":"(root)","image":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-0.png","order":0},{"name":"view-2","image":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-1.png","order":1},{"name":"view-3","image":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-2.png","order":2},{"name":"view-4","image":"https://storage.vp0.com/vp0/screens/fc9cccd0-3f0c-4312-bd8b-30093c4baf91/bc27eeac-d3aa-4a94-9f3c-667a38d07e7b/screen-3.png","order":3}],"targets":[{"name":"react-native","entrypoint":"App.tsx"},{"name":"swiftui","entrypoint":"WorkoutView.swift"}],"version":"2.0.0","category":"Fitness","platform":"ios","createdAt":"2026-05-25T19:40:49.594Z","createdBy":"vp0","importUrl":"https://vp0.com/source/forge-workout","styleTags":["Dark","Energetic"],"updatedAt":"2026-10-12T13:25:49.123Z","categories":["Fitness"],"promptText":"Build this iOS app design in my project: \"Forge — Workout\" — A strength training app: today's session with volume and per exercise rings, exercise pages where you log sets with a rest timer, a live session timer, progress with estimated one rep max and records, and training plans..\n1. Fetch https://api.vp0.com/designs/forge-workout/manifest — a JSON manifest whose files[] array contains EVERY source file inline (path + content).\n2. Create each file at its exact files[].path and paste its content VERBATIM. Do not re-imagine or restyle the UI — the provided code IS the design.\n3. Install dependencies exactly as given by the manifest's installCommand field (skip if null).\n4. Render the entry component (targets[].entrypoint) from a screen. Keep SafeAreaView / safe-area insets and the existing styling untouched.\n5. It is a UI starter: once it renders pixel-perfect, wire my own data, navigation and backend on top.\nHuman-readable source page: https://vp0.com/source/forge-workout","sourceHash":"sha256:95f9eed54140290c0511825d25355481cb05fedf662fa761b28a2715cff4e1b8","contentType":"template","description":"A strength training app: today's session with volume and per exercise rings, exercise pages where you log sets with a rest timer, a live session timer, progress with estimated one rep max and records, and training plans.","manifestUrl":"https://api.vp0.com/designs/forge-workout/manifest","sourceToken":"vp0_1f4d344d3691ccdcc03c7860","dependencies":{},"installHints":{"cli":"npx vp0com add forge-workout --target react-native","mcp":"claude mcp add vp0 -- npx -y vp0-mcp","shadcn":"npx shadcn@latest add https://api.vp0.com/r/forge-workout.json","openInV0":"https://v0.dev/chat/api/open?url=https://api.vp0.com/r/forge-workout.json","downloadExpo":"https://api.vp0.com/export/forge-workout/expo.zip","defaultTarget":"react-native","downloadXcode":"https://api.vp0.com/export/forge-workout/xcode.zip"},"schemaVersion":"vp0-bundle.v1","aiInstructions":"This is an iOS app UI starter for Expo React Native (\"Forge — Workout\").\nIntegrate it into the user's Expo project as follows:\n1. No extra dependencies are required.\n2. Create the following file(s) and paste each provided \"content\" exactly:\n   - App.tsx\n   - ui/kit.tsx\n   - WorkoutView.swift\n3. Render the exported component from a screen (e.g. app/(tabs)/index.tsx). It targets iOS — keep SafeAreaView / safe-area insets and the existing styling.\n4. These are UI starters: reproduce the visual layout faithfully, then wire the user's own data/navigation/backend afterward.","componentsUsed":[],"installCommand":null,"defaultLanguage":"react-native","devDependencies":{},"availableLanguages":["react-native","swiftui"],"registryDependencies":[]}