# Integration Options

Choose the integration option that best fits your use case. Each option provides a different level of UI customization.

Looking for a guided, conversational coaching experience? See the [AI Trainer Chat](/docs/ai-trainer-chat) integration guide — a chat-based AI personal trainer with workout launching and next-session scheduling.

## Plug and Play Integration

Pre-built experiences ready to integrate. Customize colors, fonts, and certain UI/UX elements while we handle the core workout experience, motion tracking, and user flow.

### Complete UX (Main)

Display the full KinesteX experience with personalized workout plan selection based on category. Includes user survey, assessment, and personalized schedule generation. This is the easiest integration option.

**Available Plan Categories:**
| Plan Category | Key |
|---------------|-----|
| Strength | Strength |
| Cardio | Cardio |
| Weight Management | Weight Management |
| Rehabilitation | Rehabilitation |
| Custom | Custom |

**Define Plan Category**

First, define the plan category for personalized fitness goals:

_Swift (iOS)_
```swift
// Plan category for personalized fitness goals
@State private var planCategory: PlanCategory = .Cardio
```

_Kotlin (Android)_
```kotlin
// Plan category for personalized fitness goals
private var planCategory = PlanCategory.Cardio
```

_React Native_
```jsx
// Plan category for personalized fitness goals
const planCategory = PlanCategory.Cardio; // Strength, Cardio, etc.;
```

_Flutter_
```dart
// Plan category for personalized fitness goals
PlanCategory planCategory = PlanCategory.Cardio;
```

_HTML / JavaScript_
```html
// Plan category for personalized fitness goals
const planCategory = "Cardio";
```

_React (TypeScript)_
```tsx
// Plan category for personalized fitness goals
const planCategory = PlanCategory.Cardio; // Strength, Cardio, etc.
```

**Display Category View**

Display the category-based view with real-time message handling:

_Swift (iOS)_
```swift
kinestex.createCategoryView(
    planCategory: planCategory,
    user: user, // optional: can be nil
    isLoading: $isLoading,
    customParams: ["style": "dark"], // dark or light theme
    onMessageReceived: { message in
        switch message {
        case .kinestex_launched(let data):
            print("KinesteX Launched: \(data)")
        case .finished_workout(let data):
            print("Workout Finished: \(data)")
        case .exit_kinestex(let data):
            showKinesteX = false // Dismiss the view
        default:
            print("Received \(message)")
            break
        }
    }
)
// OPTIONAL: Display loading screen during view initialization
.overlay(
    Group {
        if showAnimation {
            Text("Aifying workouts...")
                .foregroundColor(.black)
                .font(.caption)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.white)
        }
    }
)
.onChange(of: isLoading) { newValue in
    withAnimation(.easeInOut(duration: 2.5)) {
        showAnimation = !newValue
    }
}
```

_Kotlin (Android)_
```kotlin
KinesteXSDK.createMainView(
    context = this,
    planCategory = planCategory,
    user = userDetails, // optional user details
    customParams = mutableMapOf("style" to "dark"),
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.KinestexLaunched ->
                println("KinesteX Launched")
            is WebViewMessage.FinishedWorkout ->
                println("Workout Finished: ${message.data}")
            is WebViewMessage.ExitKinestex -> finish()
            else -> println("Received: $message")
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  planCategory: planCategory,
  style: {
    style: 'dark', // or 'light'
  },
};

<KinestexSDK
  data={postData}
  integrationOption={IntegrationOption.MAIN}
  handleMessage={(type, data) => {
    switch (type) {
      case 'kinestex_launched':
        console.log('KinesteX Launched:', data);
        break;
      case 'finished_workout':
        console.log('Workout Finished:', data);
        break;
      case 'exit_kinestex':
        setShowKinesteX(false);
        break;
    }
  }}
/>
```

_Flutter_
```dart
KinesteXAIFramework.createMainView(
  isShowKinestex: showKinesteX,
  planCategory: planCategory,
  customParams: {"style": "dark"},
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    if (message is KinestexLaunched) {
      print('KinesteX Launched');
    } else if (message is FinishedWorkout) {
      print('Workout Finished: ${message.data}');
    } else if (message is ExitKinestex) {
      setState(() {
        showKinesteX.value = false;
      });
    }
  },
)
```

_HTML / JavaScript_
```html
// Add planCategory to postData
const postData = {
  // ... all initial fields
  planCategory: planCategory, // "Strength", "Cardio", etc.
};

const srcURL = "https://ai.kinestex.com";
webView.src = srcURL;
webView.onload = () => {
  sendMessage();
};

// Handle messages
window.addEventListener('message', (event) => {
  const { type, data } = event.data;
  switch (type) {
    case 'kinestex_launched':
      console.log('KinesteX Launched:', data);
      break;
    case 'exit_kinestex':
      // Hide the iframe
      break;
  }
});
```

_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, PlanCategory, type IPostData } from 'kinestex-sdk-react-ts';

// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  planCategory: PlanCategory.Cardio,
  style: {
    style: 'dark',
  },
};

<KinesteXSDK
  data={postData}
  integrationOption={IntegrationOption.MAIN}
  handleMessage={(type, data) => {
    switch (type) {
      case 'kinestex_launched':
        console.log('KinesteX Launched:', data);
        break;
      case 'finished_workout':
        console.log('Workout Finished:', data);
        break;
      case 'exit_kinestex':
        setShowKinesteX(false);
        break;
    }
  }}
/>
```

**Complete Example**

Full implementation example with all required setup:

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct MainViewIntegration: View {
    @State private var showKinesteX = false
    @State private var isLoading = false

    // Replace with your KinesteX credentials
    let kinesteXKit = KinesteXAIKit(
        apiKey: "YOUR API KEY",
        companyName: "YOUR COMPANY NAME",
        userId: "YOUR USER ID"
    )

    // Plan category for personalized fitness goals
    @State private var planCategory: PlanCategory = .Cardio

    var body: some View {
        VStack {
            Text("KinesteX Main View")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showKinesteX.toggle()
            }) {
                Text("Open Main View")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.green.cornerRadius(10))
                    .padding(.horizontal)
            }

            Spacer()
        }
        .fullScreenCover(isPresented: $showKinesteX) {
            kinestex.createCategoryView(
                planCategory: planCategory,
                user: nil,
                isLoading: $isLoading,
                customParams: ["style": "light"],
                onMessageReceived: { message in
                    switch message {
                    case .exit_kinestex(_):
                        showKinesteX = false
                    default:
                        print("Message received: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    MainViewIntegration()
}
```

_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.mutableStateOf
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PlanCategory
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow

class MainViewActivity : ComponentActivity(), PermissionHandler {
    private val viewModel = MainViewModel()

    // OPTIONAL: UserDetails to customize workout intensity and calorie estimation
    // Note: User details are only used on-device during the session
    private val userDetails = UserDetails(
        age = 30,
        height = 180,
        weight = 75,
        gender = Gender.MALE,
        lifestyle = Lifestyle.ACTIVE
    )

    // Custom data for the WebView
    private val data = mutableMapOf<String, Any>()

    // Store reference to the KinesteX WebView
    private var kinesteXWebView: GenericWebView? = null

    // Register permission launcher
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        // Pass permission result to KinesteX webview
        kinesteXWebView?.handlePermissionResult(isGranted)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        data["style"] = "light"

        setContent {
            val webView = KinesteXSDK.createMainView(
                context = this,
                planCategory = PlanCategory.Cardio,
                user = userDetails,
                customParams = data,
                isLoading = viewModel.isLoading,
                onMessageReceived = ::handleWebViewMessage,
                permissionHandler = this
            ) as GenericWebView
            kinesteXWebView = webView
            webView.Render()
        }
    }

    private fun handleWebViewMessage(message: WebViewMessage) {
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
            else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
        }
    }

    // When request is sent, display system dialog for camera access
    override fun requestCameraPermission() {
        requestPermissionLauncher.launch(Manifest.permission.CAMERA)
    }
}

class MainViewModel {
    val isLoading = MutableStateFlow(true)
}
```

_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData, PlanCategory } from 'kinestex-sdk-react-native/src/types';

const MainViewIntegration = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    planCategory: PlanCategory.Cardio,
    style: {
      style: 'dark',
    },
  };

  return (
    <View style={styles.container}>
      {showKinesteX ? (
        <KinestexSDK
          data={postData}
          integrationOption={IntegrationOption.MAIN}
          handleMessage={(type, data) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <Button
          title="Open Main View"
          onPress={() => setShowKinesteX(true)}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 }
});

export default MainViewIntegration;
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Main View',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      _showCameraAccessDeniedAlert();
    }
  }

  void _showCameraAccessDeniedAlert() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Camera Permission Denied"),
          content: const Text("Camera access is required for this app to function properly."),
          actions: <Widget>[
            TextButton(
              child: const Text("OK"),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        showKinesteX.value = false;
      });
    }
  }

  Widget createMainView() {
    return Center(
      child: KinesteXAIFramework.createMainView(
        isShowKinestex: showKinesteX,
        planCategory: PlanCategory.Cardio,
        customParams: {
          "style": "dark",
        },
        isLoading: ValueNotifier<bool>(false),
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: showKinesteX,
      builder: (context, isShowKinesteX, child) {
        return isShowKinesteX
            ? SafeArea(
                child: createMainView(),
              )
            : Scaffold(
                body: Center(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 40,
                        vertical: 20,
                      ),
                      backgroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10),
                      ),
                    ),
                    onPressed: () {
                      showKinesteX.value = true;
                    },
                    child: const Text(
                      'Start Main View',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              );
      },
    );
  }
}
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: Complete User Experience</title>
  </head>
  <body>
    <button id="startKinesteX">Start KinesteX</button>
    <div
      id="webViewContainer"
      style="
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        display: none;
      "
    >
      <iframe
        id="webView"
        frameborder="0"
        allow="camera; autoplay; accelerometer; gyroscope; magnetometer"
        sandbox="allow-same-origin allow-scripts"
        style="width: 100%; height: 100%"
      ></iframe>
    </div>

    <script>
      const postData = {
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY",
        key: "YOUR_API_KEY",
        style: "dark",
        planC: "Strength",
      };

      const webViewContainer = document.getElementById("webViewContainer");
      const webView = document.getElementById("webView");
      const startButton = document.getElementById("startKinesteX");
      const srcURL = "https://ai.kinestex.com";

      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      startButton.addEventListener("click", () => {
        const isVisible = webViewContainer.style.display !== "none";
        webViewContainer.style.display = isVisible ? "none" : "block";

        if (!isVisible) {
          webView.src = srcURL;
          webView.onload = () => {
            sendMessage();
          };
        }
      });

      window.addEventListener("message", (event) => {
        if (event.origin !== "https://ai.kinestex.com") return;

        try {
          const message = JSON.parse(event.data);
          switch (message.type) {
            case "kinestex_loaded":
              sendMessage();
              break;
            case "exercise_completed":
              console.log("Exercise completed:", message.data);
              break;
            case "plan_unlocked":
              console.log("Workout plan unlocked:", message.data);
              break;
            case "exit_kinestex":
              webViewContainer.style.display = "none";
              break;
            default:
              console.log("Unhandled message:", message);
          }
        } catch (e) {
          console.error("Failed to parse message:", e);
        }
      });
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  PlanCategory,
  type IPostData,
} from 'kinestex-sdk-react-ts';

const MainViewIntegration: React.FC = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    planCategory: PlanCategory.Cardio,
    style: {
      style: 'dark',
    },
  };

  return (
    <div style={{ height: '100vh', width: '100vw' }}>
      {showKinesteX ? (
        <KinesteXSDK
          data={postData}
          integrationOption={IntegrationOption.MAIN}
          handleMessage={(type, data) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <button onClick={() => setShowKinesteX(true)}>
          Open Main View
        </button>
      )}
    </div>
  );
};

export default MainViewIntegration;
```

### Workout View

Personalized Workouts: Anytime, Anywhere.

- **Tailored for All Levels**: Workouts for strength, flexibility, or relaxation
- **Time-Saving**: Quick, efficient sessions with zero hassle
- **Engaging**: Keep users motivated with fresh, personalized routines
- **Easy Integration**: Add workouts seamlessly with minimal effort

You can find workouts in our [workout library](https://workout-view.kinestex.com/?tab=workouts), or create your own workouts in our [admin portal](https://admin.kinestex.com).

**Workout Integration**

Display a workout by name or ID:

_Swift (iOS)_
```swift
kinestex.createWorkoutView(
    workout: selectedWorkout, // workout name or ID
    user: nil,
    isLoading: $isLoading,
    customParams: ["style": "dark", "language": "en"], // dark or light theme
    onMessageReceived: { message in
        switch message {
        case .exit_kinestex(_):
            showKinesteX = false // dismiss the view
        default:
            print("Received \(message)")
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
KinesteXSDK.createWorkoutView(
    context = this,
    workoutName = selectedWorkout, // workout name or ID
    user = userDetails, // optional user details
    customParams = mutableMapOf("style" to "dark", "language" to "en"),
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            else -> println("Received: $message")
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  style: {
    style: 'dark',
  },
};

<KinestexSDK
  data={postData}
  integrationOption={IntegrationOption.WORKOUT}
  workout={selectedWorkout} // workout name or ID (passed as prop)
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

_Flutter_
```dart
KinesteXAIFramework.createWorkoutView(
  isShowKinestex: showKinesteX,
  workoutName: selectedWorkout, // workout name or ID
  customParams: {"style": "dark", "language": "en"},
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    if (message is ExitKinestex) {
      setState(() => showKinesteX.value = false);
    }
  },
)
```

_HTML / JavaScript_
```html
// Specify workout ID in the URL
const srcURL = "https://ai.kinestex.com/workout/YOUR_WORKOUT_ID";
webView.src = srcURL;
```

_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';

// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  style: {
    style: 'dark',
  },
};

<KinesteXSDK
  data={postData}
  integrationOption={IntegrationOption.WORKOUT}
  workout={selectedWorkout} // workout name or ID (passed as prop)
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

**Complete Example**

Full implementation example with workout selection:

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct WorkoutIntegrationView: View {
    @State private var showKinesteX = false
    @State private var isLoading = false

    // Initialize KinesteXAIKit
    // Replace with your KinesteX credentials
    let kinesteXKit = KinesteXAIKit(
        apiKey: "YOUR API KEY",
        companyName: "YOUR COMPANY NAME",
        userId: "YOUR USER ID"
    )

    // Replace with the name or ID of the workout
    let workoutName = "Fitness Lite"

    var body: some View {
        VStack {
            Text("KinesteX Workout Integration")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showKinesteX.toggle()
            }) {
                Text("Start \(workoutName) Workout")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.green.cornerRadius(10))
                    .padding(.horizontal)
            }
            .padding()

            Spacer()
        }
        .fullScreenCover(isPresented: $showKinesteX) {
            kinesteXKit.createWorkoutView(
                workout: workoutName,
                user: nil,
                isLoading: $isLoading,
                customParams: ["style": "dark"],
                onMessageReceived: { message in
                    switch message {
                    case .exit_kinestex(_):
                        showKinesteX = false
                    default:
                        print("Message received: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    WorkoutIntegrationView()
}
```

_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow

class WorkoutViewActivity : ComponentActivity(), PermissionHandler {
    private val viewModel = WorkoutViewModel()

    // OPTIONAL: UserDetails to customize workout intensity and calorie estimation
    // Note: User details are only used on-device during the session
    private val userDetails = UserDetails(
        age = 30,
        height = 180,
        weight = 75,
        gender = Gender.MALE,
        lifestyle = Lifestyle.ACTIVE
    )

    // Custom data for the WebView
    private val data = mutableMapOf<String, Any>()

    // Store reference to the KinesteX WebView
    private var kinesteXWebView: GenericWebView? = null

    // Register permission launcher
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        // Pass permission result to KinesteX webview
        kinesteXWebView?.handlePermissionResult(isGranted)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        data["style"] = "dark"

        setContent {
            val webView = KinesteXSDK.createWorkoutView(
                context = this,
                workoutName = "Fitness Lite",
                user = userDetails,
                customParams = data,
                isLoading = viewModel.isLoading,
                onMessageReceived = ::handleWebViewMessage,
                permissionHandler = this
            ) as GenericWebView
            kinesteXWebView = webView
            webView.Render()
        }
    }

    private fun handleWebViewMessage(message: WebViewMessage) {
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
            else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
        }
    }

    // When request is sent, display system dialog for camera access
    override fun requestCameraPermission() {
        requestPermissionLauncher.launch(Manifest.permission.CAMERA)
    }
}

class WorkoutViewModel {
    val isLoading = MutableStateFlow(true)
}
```

_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';

const WorkoutIntegration = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);
  const selectedWorkout = "Fitness Lite";

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    style: {
      style: 'dark',
    },
  };

  return (
    <View style={styles.container}>
      {showKinesteX ? (
        <KinestexSDK
          data={postData}
          integrationOption={IntegrationOption.WORKOUT}
          workout={selectedWorkout}
          handleMessage={(type) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <Button
          title={`Start ${selectedWorkout}`}
          onPress={() => setShowKinesteX(true)}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 }
});

export default WorkoutIntegration;
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Workout',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      _showCameraAccessDeniedAlert();
    }
  }

  void _showCameraAccessDeniedAlert() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Camera Permission Denied"),
          content: const Text("Camera access is required for this app to function properly."),
          actions: <Widget>[
            TextButton(
              child: const Text("OK"),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        showKinesteX.value = false;
      });
    }
  }

  Widget createWorkoutView() {
    return Center(
      child: KinesteXAIFramework.createWorkoutView(
        isShowKinestex: showKinesteX,
        workoutName: "Fitness Lite",
        customParams: {
          "style": "dark",
        },
        isLoading: ValueNotifier<bool>(false),
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: showKinesteX,
      builder: (context, isShowKinesteX, child) {
        return isShowKinesteX
            ? SafeArea(
                child: createWorkoutView(),
              )
            : Scaffold(
                body: Center(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 40,
                        vertical: 20,
                      ),
                      backgroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10),
                      ),
                    ),
                    onPressed: () {
                      showKinesteX.value = true;
                    },
                    child: const Text(
                      'Start Workout',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              );
      },
    );
  }
}
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: Workout</title>
  </head>
  <body>
    <button id="startKinesteX">Start KinesteX</button>
    <div id="webViewContainer" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none;">
      <iframe id="webView" frameborder="0" allow="camera; autoplay; accelerometer; gyroscope; magnetometer" sandbox="allow-same-origin allow-scripts" style="width: 100%; height: 100%"></iframe>
    </div>
    <script>
      const webView = document.getElementById("webView");
      const webViewContainer = document.getElementById("webViewContainer");

      // Configuration data - replace with your actual credentials
      const postData = {
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY",
        key: "YOUR_API_KEY",
        style: "dark",
      };

      // IMPORTANT: The workout ID is part of the URL path
      const workoutID = "YOUR_WORKOUT_ID"; // Replace with your actual workout ID
      const srcURL = `https://ai.kinestex.com/workout/${workoutID}`;

      // Send message with retry logic for Apple compatibility
      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      document.getElementById("startKinesteX").addEventListener("click", function () {
        webViewContainer.style.display = "block";
        webView.src = srcURL;
        webView.onload = sendMessage;
      });

      window.addEventListener("message", function (event) {
        if (event.origin !== "https://ai.kinestex.com") return;

        const message = JSON.parse(event.data);
        switch (message.type) {
          case "kinestex_launched":
            console.log("KinesteX is launched");
            break;
          case "kinestex_loaded":
            sendMessage();
            break;
          case "exit_kinestex":
            webViewContainer.style.display = "none";
            console.log("User exited the KinesteX view");
            break;
          // Handle other message types...
        }
      });
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
} from 'kinestex-sdk-react-ts';

const WorkoutIntegration: React.FC = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);
  const selectedWorkout = "Fitness Lite";

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    style: {
      style: 'dark',
    },
  };

  return (
    <div style={{ height: '100vh', width: '100vw' }}>
      {showKinesteX ? (
        <KinesteXSDK
          data={postData}
          integrationOption={IntegrationOption.WORKOUT}
          workout={selectedWorkout}
          handleMessage={(type) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <button onClick={() => setShowKinesteX(true)}>
          Start {selectedWorkout}
        </button>
      )}
    </div>
  );
};

export default WorkoutIntegration;
```

### Plan View

Display a specific workout plan with multiple workouts. Shows plan overview, schedule, and individual workout access.

**Key Features of Our Workout Plans:**
- **Goal-Oriented**: Supports strength, flexibility, and wellness goals
- **Seamless Experience**: From recommendations to real-time feedback
- **Customizable**: Brand-aligned app design
- **Quick Integration**: Easy setup for advanced fitness solutions
You can find plans in our [workout library](https://workout-view.kinestex.com/?tab=plans), or create your own plans in our [admin portal](https://admin.kinestex.com).

**Plan Integration**

Display a workout plan by name or ID:

_Swift (iOS)_
```swift
kinestex.createPlanView(
    plan: selectedPlan, // name or ID of the plan (string)
    user: nil, // OPTIONAL: provide user details
    isLoading: $isLoading,
    customParams: ["style": "dark"], // dark or light theme
    onMessageReceived: { message in
        switch message {
        case .exit_kinestex(_):
            showKinesteX = false // dismiss the view
        default:
            print("Received \(message)")
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
KinesteXSDK.createPlanView(
    context = this,
    planName = selectedPlan, // name or ID of the plan
    user = userDetails, // optional user details
    customParams = mutableMapOf("style" to "dark"),
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            else -> println("Received: $message")
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  style: {
    style: 'dark',
  },
};

<KinestexSDK
  data={postData}
  integrationOption={IntegrationOption.PLAN}
  plan={selectedPlan} // name or ID of the plan (passed as prop)
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

_Flutter_
```dart
KinesteXAIFramework.createPlanView(
  isShowKinestex: showKinesteX,
  planName: selectedPlan, // name or ID of the plan
  customParams: {"style": "dark"},
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    if (message is ExitKinestex) {
      setState(() => showKinesteX.value = false);
    }
  },
)
```

_HTML / JavaScript_
```html
// Specify plan ID in the URL
const srcURL = "https://ai.kinestex.com/plan/YOUR_PLAN_ID";
webView.src = srcURL;
```

_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';

// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  style: {
    style: 'dark',
  },
};

<KinesteXSDK
  data={postData}
  integrationOption={IntegrationOption.PLAN}
  plan={selectedPlan} // name or ID of the plan (passed as prop)
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

**Complete Example**

Full implementation example with plan selection:

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct PlanViewIntegration: View {
    @State private var showKinesteX = false
    @State private var isLoading = false
    @State private var selectedPlan = "livggb4P6zoD94VsTBB6"

    let kinestex = KinesteXAIKit(
        apiKey: "YOUR API KEY",
        companyName: "YOUR COMPANY NAME",
        userId: "YOUR USER ID"
    )

    var body: some View {
        VStack {
            Text("Select a Plan")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showKinesteX.toggle()
            }) {
                Text("Start \(selectedPlan)")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.green.cornerRadius(10))
                    .padding(.horizontal)
            }

            Spacer()
        }
        .fullScreenCover(isPresented: $showKinesteX) {
            kinestex.createPlanView(
                plan: selectedPlan,
                user: nil,
                isLoading: $isLoading,
                customParams: ["style": "light"],
                onMessageReceived: { message in
                    switch message {
                    case .exit_kinestex(_):
                        showKinesteX = false
                    default:
                        print("Message: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    PlanViewIntegration()
}
```

_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow

class PlanViewActivity : ComponentActivity(), PermissionHandler {
    private val viewModel = PlanViewModel()

    // OPTIONAL: UserDetails to customize workout intensity and calorie estimation
    // Note: User details are only used on-device during the session
    private val userDetails = UserDetails(
        age = 30,
        height = 180,
        weight = 75,
        gender = Gender.MALE,
        lifestyle = Lifestyle.ACTIVE
    )

    // Custom data for the WebView
    private val data = mutableMapOf<String, Any>()

    // Store reference to the KinesteX WebView
    private var kinesteXWebView: GenericWebView? = null

    // Register permission launcher
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        // Pass permission result to KinesteX webview
        kinesteXWebView?.handlePermissionResult(isGranted)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        data["style"] = "light"

        setContent {
            val webView = KinesteXSDK.createPlanView(
                context = this,
                planName = "livggb4P6zoD94VsTBB6", // name or ID of the plan
                user = userDetails,
                customParams = data,
                isLoading = viewModel.isLoading,
                onMessageReceived = ::handleWebViewMessage,
                permissionHandler = this
            ) as GenericWebView
            kinesteXWebView = webView
            webView.Render()
        }
    }

    private fun handleWebViewMessage(message: WebViewMessage) {
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
            else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
        }
    }

    // When request is sent, display system dialog for camera access
    override fun requestCameraPermission() {
        requestPermissionLauncher.launch(Manifest.permission.CAMERA)
    }
}

class PlanViewModel {
    val isLoading = MutableStateFlow(true)
}
```

_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';

const PlanViewIntegration = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);
  const selectedPlan = "livggb4P6zoD94VsTBB6"; // plan ID

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    style: {
      style: 'dark',
    },
  };

  return (
    <View style={styles.container}>
      {showKinesteX ? (
        <KinestexSDK
          data={postData}
          integrationOption={IntegrationOption.PLAN}
          plan={selectedPlan}
          handleMessage={(type) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <Button
          title={`Start ${selectedPlan}`}
          onPress={() => setShowKinesteX(true)}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 }
});

export default PlanViewIntegration;
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Plan',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      _showCameraAccessDeniedAlert();
    }
  }

  void _showCameraAccessDeniedAlert() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Camera Permission Denied"),
          content: const Text("Camera access is required for this app to function properly."),
          actions: <Widget>[
            TextButton(
              child: const Text("OK"),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        showKinesteX.value = false;
      });
    }
  }

  Widget createPlanView() {
    return Center(
      child: KinesteXAIFramework.createPlanView(
        isShowKinestex: showKinesteX,
        planName: "Full Body Fitness",
        customParams: {
          "style": "dark",
        },
        isLoading: ValueNotifier<bool>(false),
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: showKinesteX,
      builder: (context, isShowKinesteX, child) {
        return isShowKinesteX
            ? SafeArea(
                child: createPlanView(),
              )
            : Scaffold(
                body: Center(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 40,
                        vertical: 20,
                      ),
                      backgroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10),
                      ),
                    ),
                    onPressed: () {
                      showKinesteX.value = true;
                    },
                    child: const Text(
                      'Start Plan',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              );
      },
    );
  }
}
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: Plan</title>
  </head>
  <body>
    <button id="startKinesteX">Start KinesteX</button>
    <div id="webViewContainer" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none;">
      <iframe id="webView" frameborder="0" allow="camera; autoplay; accelerometer; gyroscope; magnetometer" sandbox="allow-same-origin allow-scripts" style="width: 100%; height: 100%"></iframe>
    </div>
    <script>
      const webView = document.getElementById("webView");
      const webViewContainer = document.getElementById("webViewContainer");

      // Configuration data - replace with your actual credentials
      const postData = {
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY",
        key: "YOUR_API_KEY",
        style: "dark",
      };

      // IMPORTANT: The plan ID is part of the URL path
      const planID = "YOUR_PLAN_ID"; // Replace with your actual plan ID
      const srcURL = `https://ai.kinestex.com/plan/${planID}`;

      // Send message with retry logic for Apple compatibility
      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      document.getElementById("startKinesteX").addEventListener("click", function () {
        webViewContainer.style.display = "block";
        webView.src = srcURL;
        webView.onload = sendMessage;
      });

      window.addEventListener("message", function (event) {
        if (event.origin !== "https://ai.kinestex.com") return;

        const message = JSON.parse(event.data);
        switch (message.type) {
          case "kinestex_launched":
            console.log("KinesteX is launched");
            break;
          case "kinestex_loaded":
            sendMessage();
            break;
          case "exit_kinestex":
            webViewContainer.style.display = "none";
            console.log("User exited the KinesteX view");
            break;
          // Handle other message types...
        }
      });
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
} from 'kinestex-sdk-react-ts';

const PlanViewIntegration: React.FC = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);
  const selectedPlan = "Full Body Fitness";

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    style: {
      style: 'dark',
    },
  };

  return (
    <div style={{ height: '100vh', width: '100vw' }}>
      {showKinesteX ? (
        <KinesteXSDK
          data={postData}
          integrationOption={IntegrationOption.PLAN}
          plan={selectedPlan}
          handleMessage={(type) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <button onClick={() => setShowKinesteX(true)}>
          Start {selectedPlan}
        </button>
      )}
    </div>
  );
};

export default PlanViewIntegration;
```

### Challenge View

Exciting Challenges: Drive Engagement and Motivation.

- **Fun and Competitive**: Quick challenges with leaderboards for friendly competition
- **Boost Activity**: Keep fitness exciting and rewarding for users
- **Easy Integration**: Add dynamic challenges effortlessly to your app
You can find exercises in our [exercise library](https://workout-view.kinestex.com/?tab=exercises), or create your own exercises in our [admin portal](https://admin.kinestex.com).

**Challenge Integration**

Display a challenge by exercise name or ID:

_Swift (iOS)_
```swift
kinestex.createChallengeView(
    exercise: challengeExercise, // exercise name or ID
    duration: 100, // duration of challenge in seconds
    user: nil, // Optionally pass user details
    showLeaderboard: true, // showLeaderboard prompts a user to enter a challenge at the end (true by default)
    isLoading: $isLoading,
    customParams: ["style": "dark"], // dark or light theme
    onMessageReceived: { message in
        switch message {
        case .exit_kinestex(let data):
            showKinesteX = false // dismiss the view
        default:
            print("Received \(message)")
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
KinesteXSDK.createChallengeView(
    context = this,
    exercise = challengeExercise, // exercise name or ID
    countdown = 100, // duration of challenge in seconds
    user = userDetails, // optional user details
    customParams = mutableMapOf("style" to "dark"),
    showLeaderboard = true, // show leaderboard at end (default true)
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            else -> println("Received: $message")
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// postData structure with challenge fields
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  exercise: 'Squats', // exercise name or ID
  countdown: 100, // duration in seconds
  showLeaderboard: true, // show leaderboard at end
  style: {
    style: 'dark',
  },
};

<KinestexSDK
  ref={kinestexSDKRef}
  data={postData}
  integrationOption={IntegrationOption.CHALLENGE}
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

_Flutter_
```dart
KinesteXAIFramework.createChallengeView(
  isShowKinestex: showKinesteX,
  exercise: challengeExercise, // exercise name or ID
  countdown: 100, // seconds
  showLeaderboard: true,
  customParams: {"style": "dark"},
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    if (message is ExitKinestex) {
      setState(() => showKinesteX.value = false);
    }
  },
)
```

_HTML / JavaScript_
```html
// Add challenge params to postData
const postData = {
  // ... all initial fields
  exercise: challengeExercise, // exercise name or ID
  showLeaderboard: true, // show leaderboard at end
  countdown: 100, // duration in seconds
};

const srcURL = "https://ai.kinestex.com/challenge";
webView.src = srcURL;
webView.onload = () => {
  sendMessage();
};
```

_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';

// postData structure with challenge fields
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  exercise: 'Squats', // exercise name or ID
  countdown: 100, // duration in seconds
  showLeaderboard: true, // show leaderboard at end
  style: {
    style: 'dark',
  },
};

<KinesteXSDK
  data={postData}
  integrationOption={IntegrationOption.CHALLENGE}
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

**Complete Example**

Full implementation example with challenge setup:

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct ChallengeIntegrationView: View {
    @State private var showKinesteX = false
    @State private var isLoading = false

    // Initialize KinesteXAIKit
    // Replace with your KinesteX credentials
    let kinesteXKit = KinesteXAIKit(
        apiKey: "YOUR API KEY",
        companyName: "YOUR COMPANY NAME",
        userId: "YOUR USER ID"
    )

    // Challenge parameters
    let challengeExercise = "Squats"
    let challengeDuration = 100 // Duration in seconds
    let showLeaderboardAfterChallenge = true

    var body: some View {
        VStack {
            Text("KinesteX Challenge Integration")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showKinesteX.toggle()
            }) {
                Text("Start \(challengeExercise) Challenge (\(challengeDuration)s)")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.red.cornerRadius(10))
                    .padding(.horizontal)
            }
            .padding()

            Spacer()
        }
        .fullScreenCover(isPresented: $showKinesteX) {
            kinesteXKit.createChallengeView(
                exercise: challengeExercise,
                duration: challengeDuration,
                showLeaderboard: showLeaderboardAfterChallenge,
                user: nil,
                isLoading: $isLoading,
                customParams: ["style": "dark"],
                onMessageReceived: { message in
                    switch message {
                    case .exit_kinestex(_):
                        showKinesteX = false
                    default:
                        print("Message received: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    ChallengeIntegrationView()
}
```

_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow

class ChallengeViewActivity : ComponentActivity(), PermissionHandler {
    private val viewModel = ChallengeViewModel()

    // OPTIONAL: UserDetails to customize workout intensity and calorie estimation
    // Note: User details are only used on-device during the session
    private val userDetails = UserDetails(
        age = 30,
        height = 180,
        weight = 75,
        gender = Gender.MALE,
        lifestyle = Lifestyle.ACTIVE
    )

    // Custom data for the WebView
    private val data = mutableMapOf<String, Any>()

    // Store reference to the KinesteX WebView
    private var kinesteXWebView: GenericWebView? = null

    // Register permission launcher
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        // Pass permission result to KinesteX webview
        kinesteXWebView?.handlePermissionResult(isGranted)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        data["style"] = "dark"

        setContent {
            val webView = KinesteXSDK.createChallengeView(
                context = this,
                exercise = "Squats",
                countdown = 60,
                user = userDetails,
                customParams = data,
                showLeaderboard = true,
                isLoading = viewModel.isLoading,
                onMessageReceived = ::handleWebViewMessage,
                permissionHandler = this
            ) as GenericWebView
            kinesteXWebView = webView
            webView.Render()
        }
    }

    private fun handleWebViewMessage(message: WebViewMessage) {
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
            else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
        }
    }

    // When request is sent, display system dialog for camera access
    override fun requestCameraPermission() {
        requestPermissionLauncher.launch(Manifest.permission.CAMERA)
    }
}

class ChallengeViewModel {
    val isLoading = MutableStateFlow(true)
}
```

_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';

const ChallengeIntegration = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);
  const challengeExercise = "Squats";
  const challengeDuration = 100;

  // Include challenge fields in postData
  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    exercise: challengeExercise, // exercise name or ID
    countdown: challengeDuration, // duration in seconds
    showLeaderboard: true, // show leaderboard at end
    style: {
      style: 'dark',
    },
  };

  return (
    <View style={styles.container}>
      {showKinesteX ? (
        <KinestexSDK
          data={postData}
          integrationOption={IntegrationOption.CHALLENGE}
          handleMessage={(type) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <Button
          title={`Start ${challengeExercise} Challenge (${challengeDuration}s)`}
          onPress={() => setShowKinesteX(true)}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 }
});

export default ChallengeIntegration;
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Challenge',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);
  String challengeExercise = "Squats";
  int challengeDuration = 100;

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      _showCameraAccessDeniedAlert();
    }
  }

  void _showCameraAccessDeniedAlert() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Camera Permission Denied"),
          content: const Text("Camera access is required for this app to function properly."),
          actions: <Widget>[
            TextButton(
              child: const Text("OK"),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        showKinesteX.value = false;
      });
    }
  }

  Widget createChallengeView() {
    return Center(
      child: KinesteXAIFramework.createChallengeView(
        isShowKinestex: showKinesteX,
        exercise: challengeExercise,
        countdown: challengeDuration,
        showLeaderboard: true,
        customParams: {
          "style": "dark",
        },
        isLoading: ValueNotifier<bool>(false),
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: showKinesteX,
      builder: (context, isShowKinesteX, child) {
        return isShowKinesteX
            ? SafeArea(
                child: createChallengeView(),
              )
            : Scaffold(
                body: Center(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 40,
                        vertical: 20,
                      ),
                      backgroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10),
                      ),
                    ),
                    onPressed: () {
                      showKinesteX.value = true;
                    },
                    child: Text(
                      'Start $challengeExercise Challenge (${challengeDuration}s)',
                      style: const TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              );
      },
    );
  }
}
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: Challenge</title>
  </head>
  <body>
    <button id="startKinesteX">Start KinesteX</button>
    <div id="webViewContainer" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none;">
      <iframe id="webView" frameborder="0" allow="camera; autoplay; accelerometer; gyroscope; magnetometer" sandbox="allow-same-origin allow-scripts" style="width: 100%; height: 100%"></iframe>
    </div>
    <script>
      const webView = document.getElementById("webView");
      const webViewContainer = document.getElementById("webViewContainer");

      const postData = {
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY",
        key: "YOUR_API_KEY",
        style: "dark",
        exercise: "Squats",
        showLeaderboard: true,
        countdown: 100,
      };

      const srcURL = "https://ai.kinestex.com/challenge";

      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      document.getElementById("startKinesteX").addEventListener("click", function () {
        webViewContainer.style.display = "block";
        webView.src = srcURL;
        webView.onload = sendMessage;
      });

      window.addEventListener("message", function (event) {
        if (event.origin !== "https://ai.kinestex.com") return;

        const message = JSON.parse(event.data);
        switch (message.type) {
          case "kinestex_launched":
            console.log("KinesteX is launched");
            break;
          case "kinestex_loaded":
            sendMessage();
            break;
          case "exit_kinestex":
            webViewContainer.style.display = "none";
            console.log("User exited the KinesteX view");
            break;
          // Handle other message types...
        }
      });
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
} from 'kinestex-sdk-react-ts';

const ChallengeIntegration: React.FC = () => {
  const [showKinesteX, setShowKinesteX] = useState(false);
  const challengeExercise = "Squats";
  const challengeDuration = 100;

  // Include challenge fields in postData
  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    exercise: challengeExercise, // exercise name or ID
    countdown: challengeDuration, // duration in seconds
    showLeaderboard: true, // show leaderboard at end
    style: {
      style: 'dark',
    },
  };

  return (
    <div style={{ height: '100vh', width: '100vw' }}>
      {showKinesteX ? (
        <KinesteXSDK
          data={postData}
          integrationOption={IntegrationOption.CHALLENGE}
          handleMessage={(type) => {
            if (type === 'exit_kinestex') {
              setShowKinesteX(false);
            }
          }}
        />
      ) : (
        <button onClick={() => setShowKinesteX(true)}>
          Start {challengeExercise} Challenge ({challengeDuration}s)
        </button>
      )}
    </div>
  );
};

export default ChallengeIntegration;
```

### Leaderboard View (Challenge)

Ready-made Leaderboard: Boost User Engagement and Motivation.

- **Adaptive Design**: The leaderboard automatically adapts to your KinesteX UI and can be fully customized in the admin dashboard
- **Real-time Updates**: Whenever a new ranking is available, the leaderboard automatically refreshes to show the latest standings

**Leaderboard Integration**

Display the leaderboard for a specific exercise:

_Swift (iOS)_
```swift
kinestex.createLeaderboardView(
    exercise: "Squats", // Specify the exercise id or title
    username: "", // if you know the username: highlight the user by specifying their username
    isLoading: $isLoading,
    customParams: [
        "style": "dark", // light or dark theme (default is dark)
        "isHideHeaderMain": true // OPTIONAL: hide the exit button from the leaderboard
    ],
    onMessageReceived: { message in
        switch message {
        case .exit_kinestex(_):
            showKinesteX = false
        default:
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
KinesteXSDK.createLeaderboardView(
    context = this,
    exercise = "Squats", // exercise name or ID
    username = "John", // highlight username in leaderboard if known
    customParams = mutableMapOf(
        "style" to "dark",
        "isHideHeaderMain" to true // hide exit button
    ),
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            else -> println("Received: $message")
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  exercise: 'Squats', // exercise name or ID
  style: {
    style: 'dark',
  },
};

<KinestexSDK
  data={postData}
  integrationOption={IntegrationOption.LEADERBOARD}
  username="John" // highlight this user in leaderboard (passed as prop)
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

_Flutter_
```dart
KinesteXAIFramework.createLeaderboardView(
  isShowKinestex: showKinesteX,
  exercise: "Squats",
  username: "", // highlight user if known
  customParams: {
    "style": "dark",
    "isHideHeaderMain": true, // hide exit button
  },
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    if (message is ExitKinestex) {
      setState(() => showKinesteX.value = false);
    }
  },
)
```

_HTML / JavaScript_
```html
// Add to postData
const postData = {
  // ... all initial fields
  exercise: "Squats", // exercise name or ID
};

const userId = "unique-user-id"; // OPTIONAL: userId to highlight in leaderboard
const srcURL = `https://ai.kinestex.com/leaderboard/?userId=${userId}`;
webView.src = srcURL;
webView.onload = () => {
  sendMessage();
};
```

_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';

// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  exercise: 'Squats', // exercise ID or name 
  style: {
    style: 'dark',
  },
};

<KinesteXSDK
  data={postData}
  integrationOption={IntegrationOption.LEADERBOARD}
  username="John" // highlight this user in leaderboard (passed as prop)
  handleMessage={(type, data) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  }}
/>
```

### AI Experiences

AI-powered movement-based games and clinical assessments with real-time motion tracking.

**AI Games:**
| Game | Exercise ID |
|------|-------------|
| Balloon Pop | balloonpop |
| Color Chase | colorchase |
| Alien Squat Shooter | aliensquatshooter |

**Clinical Assessments:**
| Assessment | Exercise ID |
|------------|-------------|
| Timed Up and Go (TUG) | tug |
| Gait Speed Test | gaitspeedtest |
| Sit-to-Stand | sittostand |
| Functional Reach Test | functionalreachtest |
| Single Leg Stance Test | singlelegstancetest |
| Five Times Sit-to-Stand | fivetimessts |
| Side-by-Side Stand | sidebysidestand |
| Semi-Tandem Stand | semitandemstand |
| Full Tandem Stand | fulltandem |
| Shoulder Range of Motion | romshoulder |

**Experience View**

_Swift (iOS)_
```swift
// Launch an AI game or balance assessment
// Pass the exercise ID from the tables above
kinestex.createExperienceView(
    experience: "assessment", // experience type
    exercise: "balloonpop",   // exercise ID from table
    user: nil,
    isLoading: $isLoading,
    customParams: ["style": "dark"],
    onMessageReceived: { message in
        switch message {
        case .exit_kinestex(_):
            showKinesteX = false
        default:
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
// Launch an AI game or balance assessment
val data = mutableMapOf<String, Any>()
data["style"] = "dark"
data["exercise"] = "balloonpop" // exercise ID from table

KinesteXSDK.createExperiencesView(
    context = this,
    experienceName = "assessment",
    user = userDetails, // optional user details
    customParams = data,
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        if (message is WebViewMessage.ExitKinestex) finish()
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  exercise: 'balloonpop', // exercise ID from table
  style: {
    style: 'dark',
  },
};

<KinestexSDK
  data={postData}
  integrationOption={IntegrationOption.EXPERIENCE}
  experience="assessment" // experience type (passed as prop)
  handleMessage={handleMessage}
/>
```

_Flutter_
```dart
KinesteXAIFramework.createExperienceView(
  isShowKinestex: showKinesteX,
  experience: "assessment",
  customParams: {
    "style": "dark",
    "exercise": "balloonpop", // exercise ID from table
  },
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    handleWebViewMessage(message);
  },
)
```

_HTML / JavaScript_
```html
const postData = {
  // ... all initial fields
  exercise: "balloonpop", // exercise ID from table
};

const srcURL = "https://ai.kinestex.com/experiences/assessment";
webView.src = srcURL;
webView.onload = () => {
   sendMessage();
};
```

_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';

// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  exercise: 'balloonpop', // exercise ID from table
  style: {
    style: 'dark',
  },
};

<KinesteXSDK
  data={postData}
  integrationOption={IntegrationOption.EXPERIENCE}
  experience="assessment" // experience type (passed as prop)
  handleMessage={handleMessage}
/>
```

**Complete Example**

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct ExperienceIntegrationView: View {
    @State private var showKinesteX = false
    @State private var isLoading = false

    // Initialize KinesteXAIKit
    // Replace with your KinesteX credentials
    let kinesteXKit = KinesteXAIKit(
        apiKey: "YOUR API KEY",
        companyName: "YOUR COMPANY NAME",
        userId: "YOUR USER ID"
    )

    // Parameters for the experience
    let experienceName = "assessment" // Name of the AI experience
    let experienceExercise = "balloonpop" // Exercise ID from table

    var body: some View {
        VStack {
            Text("KinesteX Experience Integration")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showKinesteX.toggle()
            }) {
                Text("Start '\(experienceName.capitalized)' Experience")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.teal.cornerRadius(10))
                    .padding(.horizontal)
            }
            .padding()

            Spacer()
        }
        .fullScreenCover(isPresented: $showKinesteX) {
            kinesteXKit.createExperienceView(
                experience: experienceName,
                exercise: experienceExercise,
                user: nil,
                isLoading: $isLoading,
                customParams: ["style": "dark"],
                onMessageReceived: { message in
                    switch message {
                    case .exit_kinestex(_):
                        showKinesteX = false
                    default:
                        print("Message received: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    ExperienceIntegrationView()
}
```

_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow

class ExperienceActivity : ComponentActivity(), PermissionHandler {
    private val viewModel = ExperienceViewModel()

    // OPTIONAL: UserDetails to customize workout intensity and calorie estimation
    // Note: User details are only used on-device during the session
    private val userDetails = UserDetails(
        age = 30,
        height = 180,
        weight = 75,
        gender = Gender.MALE,
        lifestyle = Lifestyle.ACTIVE
    )

    // Custom data for the WebView
    private val data = mutableMapOf<String, Any>()

    // Store reference to the KinesteX WebView
    private var kinesteXWebView: GenericWebView? = null

    // Register permission launcher
    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        // Pass permission result to KinesteX webview
        kinesteXWebView?.handlePermissionResult(isGranted)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        data["style"] = "dark"
        data["exercise"] = "balloonpop" // AI game or balance assessment ID

        setContent {
            val webView = KinesteXSDK.createExperiencesView(
                context = this,
                experienceName = "assessment",
                user = userDetails,
                customParams = data,
                isLoading = viewModel.isLoading,
                onMessageReceived = ::handleWebViewMessage,
                permissionHandler = this
            ) as GenericWebView
            kinesteXWebView = webView
            webView.Render()
        }
    }

    private fun handleWebViewMessage(message: WebViewMessage) {
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
            else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
        }
    }

    // When request is sent, display system dialog for camera access
    override fun requestCameraPermission() {
        requestPermissionLauncher.launch(Manifest.permission.CAMERA)
    }
}

class ExperienceViewModel {
    val isLoading = MutableStateFlow(true)
}
```

_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';

export default function ExperienceScreen() {
  const [showKinesteX, setShowKinesteX] = useState(false);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    exercise: 'balloonpop', // AI game or balance assessment ID
    style: {
      style: 'dark',
    },
  };

  const handleMessage = (type: string, data: any) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  };

  if (showKinesteX) {
    return (
      <KinestexSDK
        data={postData}
        integrationOption={IntegrationOption.EXPERIENCE}
        experience="assessment"
        handleMessage={handleMessage}
      />
    );
  }

  return (
    <View>
      <Button title="Start AI Experience" onPress={() => setShowKinesteX(true)} />
    </View>
  );
}
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Experience',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      _showCameraAccessDeniedAlert();
    }
  }

  void _showCameraAccessDeniedAlert() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Camera Permission Denied"),
          content: const Text("Camera access is required for this app to function properly."),
          actions: <Widget>[
            TextButton(
              child: const Text("OK"),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        showKinesteX.value = false;
      });
    }
  }

  Widget createExperienceView() {
    return Center(
      child: KinesteXAIFramework.createExperienceView(
        isShowKinestex: showKinesteX,
        experience: "assessment",
        customParams: {
          "style": "dark",
          "exercise": "balloonpop",
        },
        isLoading: ValueNotifier<bool>(false),
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: showKinesteX,
      builder: (context, isShowKinesteX, child) {
        return isShowKinesteX
            ? SafeArea(
                child: createExperienceView(),
              )
            : Scaffold(
                body: Center(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 40,
                        vertical: 20,
                      ),
                      backgroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10),
                      ),
                    ),
                    onPressed: () {
                      showKinesteX.value = true;
                    },
                    child: const Text(
                      'Start AI Experience',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              );
      },
    );
  }
}
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: AI Experience</title>
  </head>
  <body>
    <button id="startKinesteX">Start KinesteX</button>
    <div id="webViewContainer" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none;">
      <iframe id="webView" frameborder="0" allow="camera; autoplay; accelerometer; gyroscope; magnetometer" sandbox="allow-same-origin allow-scripts" style="width: 100%; height: 100%"></iframe>
    </div>
    <script>
      const webView = document.getElementById("webView");
      const webViewContainer = document.getElementById("webViewContainer");

      const postData = {
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY",
        key: "YOUR_API_KEY",
        style: "dark",
        exercise: "balloonpop", // AI game or balance assessment ID
      };

      // IMPORTANT: The experience type is part of the URL path
      // Use "assessment" for fitness assessments or specific game IDs
      const srcURL = "https://ai.kinestex.com/experiences/assessment";

      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      document.getElementById("startKinesteX").addEventListener("click", function () {
        webViewContainer.style.display = "block";
        webView.src = srcURL;
        webView.onload = sendMessage;
      });

      window.addEventListener("message", function (event) {
        if (event.origin !== "https://ai.kinestex.com") return;

        const message = JSON.parse(event.data);
        switch (message.type) {
          case "kinestex_launched":
            console.log("KinesteX is launched");
            break;
          case "kinestex_loaded":
            sendMessage();
            break;
          case "exit_kinestex":
            webViewContainer.style.display = "none";
            console.log("User exited the KinesteX view");
            break;
          // Handle other message types...
        }
      });
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
} from 'kinestex-sdk-react-ts';

export default function ExperienceScreen() {
  const [showKinesteX, setShowKinesteX] = useState(false);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    exercise: 'balloonpop', // AI game or balance assessment ID
    style: {
      style: 'dark',
    },
  };

  const handleMessage = (type: string, data: Record<string, unknown>) => {
    if (type === 'exit_kinestex') {
      setShowKinesteX(false);
    }
  };

  if (showKinesteX) {
    return (
      <div style={{ height: '100vh', width: '100vw' }}>
        <KinesteXSDK
          data={postData}
          integrationOption={IntegrationOption.EXPERIENCE}
          experience="assessment"
          handleMessage={handleMessage}
        />
      </div>
    );
  }

  return <button onClick={() => setShowKinesteX(true)}>Start AI Experience</button>;
}
```

### Personalized Plan View

AI-Generated Personalized Workout Plans.

- **Personalized**: Tailored to height, weight, age, gender, activity level, and fitness assessment results
- **Goal-Oriented**: Supports strength, flexibility, and wellness goals
- **Seamless Experience**: From recommendations to real-time feedback
- **Customizable**: Brand-aligned app design
- **Quick Integration**: Easy setup for advanced fitness solutions

**Personalized Plan Integration**

_Swift (iOS)_
```swift
kinestex.createPersonalizedPlanView(
    user: nil, // OPTIONAL: provide user details
    isLoading: $isLoading,
    customParams: ["style": "dark"], // dark or light theme (customizable in admin portal)
    onMessageReceived: { message in
        switch message {
        case .exit_kinestex(_):
            showKinesteX = false // dismiss the view
        default:
            print("Received \(message)")
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
KinesteXSDK.createPersonalizedPlanView(
    context = this,
    user = userDetails, // optional user details
    customParams = mutableMapOf("style" to "dark"), // customizable in admin portal
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.ExitKinestex -> finish()
            else -> println("Received: $message")
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  style: {
    style: 'dark',
  },
};

<KinestexSDK
  data={postData}
  integrationOption={IntegrationOption.PERSONALIZED_PLAN}
  handleMessage={handleMessage}
/>
```

_Flutter_
```dart
KinesteXAIFramework.createPersonalizedPlanView(
  isShowKinestex: showKinesteX,
  customParams: {"style": "dark"},
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    handleWebViewMessage(message);
  },
)
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: Personalized Plan</title>
  </head>
  <body>
    <button id="startKinesteX">Start Personalized Plan</button>
    <div id="webViewContainer" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none;">
      <iframe id="webView" frameborder="0" allow="camera; autoplay; accelerometer; gyroscope; magnetometer" sandbox="allow-same-origin allow-scripts" style="width: 100%; height: 100%"></iframe>
    </div>
    <script>
      // Get references to iframe and container at the top
      const webView = document.getElementById("webView");
      const webViewContainer = document.getElementById("webViewContainer");

      // Configuration data - replace with your actual credentials
      const postData = {
        userId: "YOUR_USER_ID",    // Unique identifier for the user
        company: "YOUR_COMPANY",   // Your company name from KinesteX
        key: "YOUR_API_KEY",       // Your API key from KinesteX
        style: "dark",             // Theme: "dark" or "light"
      };

      // IMPORTANT: This is the URL for the Personalized Plan integration
      // The personalized-plan endpoint handles user surveys, assessments, and AI-generated workout plans
      const srcURL = "https://ai.kinestex.com/personalized-plan";

      // Send configuration data to the iframe via postMessage
      // Includes retry logic for Apple Safari compatibility
      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      // Launch the KinesteX experience when button is clicked
      document.getElementById("startKinesteX").addEventListener("click", function () {
        webViewContainer.style.display = "block";
        // Set the iframe source to load the KinesteX personalized plan
        webView.src = srcURL;
        // Send configuration data once the iframe loads
        webView.onload = sendMessage;
      });

      // Listen for messages from the KinesteX iframe
      window.addEventListener("message", function (event) {
        // SECURITY: Always validate the origin of incoming messages
        if (event.origin !== "https://ai.kinestex.com") return;

        // Parse the message data from the iframe
        const message = JSON.parse(event.data);

        switch (message.type) {
          case "kinestex_launched":
            console.log("KinesteX is launched");
            break;
          case "kinestex_loaded":
            sendMessage();
            break;
          case "exit_kinestex":
            // User requested to exit - hide the iframe container
            webViewContainer.style.display = "none";
            console.log("User exited the KinesteX view");
            break;
          // Handle other message types as needed...
        }
      });
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';

// postData structure
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  style: {
    style: 'dark',
  },
};

<KinesteXSDK
  data={postData}
  integrationOption={IntegrationOption.PERSONALIZED_PLAN}
  handleMessage={handleMessage}
/>
```

### AI Trainer Chat

Conversational AI personal trainer — onboarding with an in-app fitness assessment, personalized workout generation, post-workout check-ins, and next-session scheduling in a fully white-labeled chat UI.

Full guide: [AI Trainer Chat](/docs/ai-trainer-chat)

### Admin Workout Editor

Embedded view for creating and managing workouts and exercises. As users interact with the editor, your application will receive events that you can handle to trigger custom logic.

**Available for Flutter and Swift (iOS).**

**Custom Query Options (Flutter):**
| Option | Description |
|--------|-------------|
| hidePlansTab | Hide the plans tab in the dashboard |
| tab | Default tab: "workouts", "exercises", or "plans" |
| isSelectableMenu | Show select button on cards, triggers `_selected` events |

**Available Events:**

*General Events:*
- `kinestex_loaded` - Application fully loaded
- `kinestex_launched` - Successful authentication
- `error_occurred` - Authentication error

*Exercise Events:*
- `exercise_opened` - Exercise detail page opened
- `exercise_selection_opened` - Exercise list page opened
- `exercise_selected` - Exercise selected from menu
- `exercise_saved` - Exercise created/updated
- `exercise_removed` - Exercise removed from workout

*Workout Events:*
- `workout_opened` - Workout detail page opened
- `workout_selection_opened` - Workout list page opened
- `workout_selected` - Workout selected from menu
- `workout_saved` - Workout created/updated

*Plan Events:*
- `plan_opened` - Plan detail page opened
- `plan_selection_opened` - Plan list page opened
- `plan_selected` - Plan selected from menu
- `plan_saved` - Plan created/updated

**Swift Parameters:**
| Parameter | Description |
|-----------|-------------|
| organization (required) | Your organization identifier |
| contentType (optional) | `.workout`, `.plan`, or `.exercise` |
| contentId (optional) | Specific content ID to edit |
| customQueries (optional) | Additional query parameters |

**Admin Workout Editor Integration**

Create the admin workout editor view for managing workouts and exercises:

_Swift (iOS)_
```swift
// Open main admin dashboard
kinestex.createAdminWorkoutEditor(
    organization: "YourOrg",
    isLoading: $isLoading,
    onMessageReceived: { message in
        switch message {
        case .exit_kinestex(_):
            showEditor = false
        default:
            print("Message received: \(message)")
        }
    }
)

// Open specific workout for editing
kinestex.createAdminWorkoutEditor(
    organization: "YourOrg",
    contentType: .workout,
    contentId: "workout123",
    isLoading: $isLoading,
    onMessageReceived: { message in /* handle messages */ }
)

// Open specific exercise for editing
kinestex.createAdminWorkoutEditor(
    organization: "YourOrg",
    contentType: .exercise,
    contentId: "exercise456",
    customQueries: ["language": "en"],
    isLoading: $isLoading,
    onMessageReceived: { message in /* handle messages */ }
)
```

_Flutter_
```dart
KinesteXAIFramework.createAdminWorkoutEditor(
  // Use an organization name to differentiate between different orgs.
  // If you don't plan to use multiple orgs, you can use your company name.
  organization: "your_organization_name",
  isShowKinestex: showKinesteX,
  // OPTIONAL: show/hide content on the admin dashboard
  customQueries: {
    "hidePlansTab": true, // will hide the plans tabs in the dashboard
    "tab": "workouts", // will default to workouts tab. Options: "exercises", "plans"
    "isSelectableMenu": true // show select Button, triggers _selected events
  },
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    handleWebViewMessage(message);
  },
)
```

**Complete Example**

Full implementation with event handling:

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct AdminEditorView: View {
    @State private var showEditor = false
    @State private var isLoading = false

    // Initialize KinesteXAIKit with your credentials
    let kinestex = KinesteXAIKit(
        apiKey: "YOUR_API_KEY",
        companyName: "YOUR_COMPANY_NAME",
        userId: "YOUR_USER_ID"
    )

    var body: some View {
        VStack {
            Text("Admin Editor")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showEditor.toggle()
            }) {
                Text("Open Admin Editor")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.blue.cornerRadius(10))
                    .padding(.horizontal)
            }

            Spacer()
        }
        .fullScreenCover(isPresented: $showEditor) {
            kinestex.createAdminWorkoutEditor(
                organization: "YourOrg",
                contentType: nil,
                contentId: nil,
                customQueries: nil,
                isLoading: $isLoading,
                onMessageReceived: { message in
                    switch message {
                    case .exit_kinestex(_):
                        showEditor = false
                    case .error_occurred(let data):
                        print("Error: \(data)")
                    default:
                        print("Message received: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    AdminEditorView()
}
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Admin Editor',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      _showCameraAccessDeniedAlert();
    }
  }

  void _showCameraAccessDeniedAlert() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Camera Permission Denied"),
          content: const Text("Camera access is required for this app to function properly."),
          actions: <Widget>[
            TextButton(
              child: const Text("OK"),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        showKinesteX.value = false;
      });
    } else if (message is ErrorOccurred) {
      print('Error: ${message.data["error_message"]}');
    } else {
      // Admin editor events arrive as generic messages — check data['type']
      switch (message.data['type']) {
        case 'workout_saved':
          print('Workout saved: ${message.data["workout_id"]}');
          break;
        case 'exercise_saved':
          print('Exercise saved: ${message.data["exercise_id"]}');
          break;
        case 'workout_selected':
          print('Workout selected: ${message.data["workout_title"]}');
          break;
        case 'exercise_selected':
          print('Exercise selected: ${message.data["exercise_title"]}');
          break;
        case 'plan_saved':
          print('Plan saved: ${message.data["plan_id"]}');
          break;
      }
    }
  }

  Widget createAdminWorkoutEditorView() {
    return Center(
      child: KinesteXAIFramework.createAdminWorkoutEditor(
        organization: "your_organization_name",
        isShowKinestex: showKinesteX,
        customQueries: {
          "hidePlansTab": false,
          "tab": "workouts",
          "isSelectableMenu": true,
        },
        isLoading: ValueNotifier<bool>(false),
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: showKinesteX,
      builder: (context, isShowKinesteX, child) {
        return isShowKinesteX
            ? SafeArea(
                child: createAdminWorkoutEditorView(),
              )
            : Scaffold(
                body: Center(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 40,
                        vertical: 20,
                      ),
                      backgroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10),
                      ),
                    ),
                    onPressed: () {
                      showKinesteX.value = true;
                    },
                    child: const Text(
                      'Open Admin Workout Editor',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              );
      },
    );
  }
}
```

## Custom Integration

Build everything yourself with full control over UI/UX. Use our Camera Component for real-time motion analysis and create custom workout sequences.

### Custom Workout

Create and execute personalized workout sequences with custom exercises, repetitions, durations, and rest periods. Define your own workout flow with full control over exercise order and timing.

**How it works:**
1. Initialize the SDK with custom workout integration option
2. Pass your custom workout exercises array
3. Wait for `all_resources_loaded` message
4. Send `workout_activity_action: start` to begin

**WorkoutSequenceExercise Parameters:**
- `exerciseId` - Exercise ID from KinesteX API or admin panel
- `reps` - Number of repetitions
- `duration` - Duration in seconds (null = unlimited time for reps)
- `includeRestPeriod` - Include rest period before exercise
- `restDuration` - Rest duration in seconds

**Tip:** To create sets, duplicate the same exercise in the array multiple times.

**Custom Workout Setup**

_Swift (iOS)_
```swift
@State var workoutAction: [String: Any]? = nil

let exercises = [
    WorkoutSequenceExercise(
        exerciseId: "jz73VFlUyZ9nyd64OjRb",
        reps: 15,
        duration: nil,
        includeRestPeriod: true,
        restDuration: 20
    ),
    WorkoutSequenceExercise(
        exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
        reps: 10,
        duration: 30,
        includeRestPeriod: true,
        restDuration: 15
    ),
    WorkoutSequenceExercise(
        exerciseId: "gJGOiZhCvJrhEP7sTy78",
        reps: 20,
        duration: nil,
        includeRestPeriod: false,
        restDuration: 0
    )
]

kinestex.createCustomWorkoutView(
    exercises: exercises,
    user: userDetails,
    style: nil,
    isLoading: $isLoading,
    workoutAction: $workoutAction,
    onMessageReceived: { message in
        if case .custom_type(let value) = message,
           let type = value["type"] as? String,
           type == "all_resources_loaded" {
            // Start workout when resources are ready
            workoutAction = ["workout_activity_action": "start"]
        }
    }
)
```

_Kotlin (Android)_
```kotlin
// Define custom workout exercises
val customExercises = listOf(
    WorkoutSequenceExercise(
        exerciseId = "jz73VFlUyZ9nyd64OjRb",
        reps = 15,
        duration = null,
        includeRestPeriod = true,
        restDuration = 20
    ),
    WorkoutSequenceExercise(
        exerciseId = "ZVMeLsaXQ9Tzr5JYXg29",
        reps = 10,
        duration = 30,
        includeRestPeriod = true,
        restDuration = 15
    )
)

KinesteXSDK.createCustomWorkoutView(
    context = this,
    customWorkouts = customExercises,
    user = userDetails, // optional user details
    isLoading = viewModel.isLoading,
    customParams = mutableMapOf("style" to "dark"),
    onMessageReceived = { message ->
        if (message is WebViewMessage.AllResourcesLoaded) {
            // Start workout when ready
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// Step 1: Define custom workout exercises
const customWorkoutExercises: WorkoutSequenceExercise[] = [
  {
    exerciseId: "jz73VFlUyZ9nyd64OjRb", // exercise id from kinestex api
    reps: 15,                            // number of reps
    duration: null,                      // null = unlimited time for reps
    includeRestPeriod: true,             // include rest before exercise
    restDuration: 20,                    // rest duration in seconds
  },
  {
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  },
  // Duplicate exercise to create a set
  {
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  },
];

// Step 2: Configure postData with customWorkoutExercises
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
  style: {
    style: 'dark',
  },
};

// Step 3: Handle SDK messages
const handleMessage = (type: string, data: any) => {
  switch (type) {
    case 'all_resources_loaded':
      // SDK is ready - show the view and start workout
      setAllResourcesLoaded(true);
      kinestexSDKRef.current?.sendAction("workout_activity_action", "start");
      break;
    case 'workout_exit_request':
      setAllResourcesLoaded(false);
      setShowKinestex(false);
      break;
    case 'exit_kinestex':
      setAllResourcesLoaded(false);
      break;
  }
};

// Step 4: Render with conditional visibility
<View style={[
  styles.sdkContainer,
  showKinestex && !allResourcesLoaded && styles.hiddenSdkContainer,
]}>
  {showKinestex && (
    <KinestexSDK
      ref={kinestexSDKRef}
      data={postData}
      integrationOption={IntegrationOption.CUSTOM_WORKOUT}
      handleMessage={handleMessage}
    />
  )}
</View>
```

_Flutter_
```dart
// Define custom workout exercises
final customWorkoutExercises = [
  WorkoutSequenceExercise(
    exerciseId: "jz73VFlUyZ9nyd64OjRb",
    reps: 15,
    duration: null,
    includeRestPeriod: true,
    restDuration: 20,
  ),
  WorkoutSequenceExercise(
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  ),
];

KinesteXAIFramework.createCustomWorkoutView(
  isShowKinestex: showKinesteX,
  customWorkouts: customWorkoutExercises,
  customParams: {"style": "dark"},
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    // Custom events arrive as CustomType; the event name is in data['type']
    if (message is CustomType && message.data['type'] == 'all_resources_loaded') {
      // Start workout when ready
    }
  },
)
```

_HTML / JavaScript_
```html
// Define custom workout exercises
const customWorkoutExercises = [
  {
    exerciseId: "jz73VFlUyZ9nyd64OjRb",
    reps: 15,
    duration: null,
    includeRestPeriod: true,
    restDuration: 20,
  },
  {
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  },
];

const config = {
  ...postData,
  customWorkoutExercises: customWorkoutExercises,
};

const srcURL = "https://ai.kinestex.com/custom-workout";
webView.src = srcURL;
webView.onload = () => {
  sendMessage();
};

// Listen for ready signal
window.addEventListener("message", (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === 'all_resources_loaded') {
    // Start workout
    webView.contentWindow.postMessage(
      { action: "workout_activity_action", value: "start" },
      srcURL
    );
  }
});
```

_React (TypeScript)_
```tsx
import { useRef, useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

// Define custom workout exercises
interface WorkoutSequenceExercise {
  exerciseId: string;
  reps: number | null;
  duration: number | null;
  includeRestPeriod: boolean;
  restDuration: number;
}

const customWorkoutExercises: WorkoutSequenceExercise[] = [
  {
    exerciseId: "jz73VFlUyZ9nyd64OjRb",
    reps: 15,
    duration: null,
    includeRestPeriod: true,
    restDuration: 20,
  },
];

// postData structure with customWorkoutExercises
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
  style: {
    style: 'dark',
  },
};

const ref = useRef<KinesteXSDKCamera>(null);

<KinesteXSDK
  ref={ref}
  data={postData}
  integrationOption={IntegrationOption.CUSTOM_WORKOUT}
  handleMessage={(type, data) => {
    if (type === 'all_resources_loaded') {
      ref.current?.sendAction("workout_activity_action", "start");
    }
  }}
/>
```

#### Complete Example

Full implementation with state management, loading indicators, and proper message handling.

**Complete Implementation**

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct CustomWorkoutView: View {
    @State private var showKinesteX = false
    @State private var isLoading = false
    @State private var allResourcesLoaded = false
    @State var workoutAction: [String: Any]? = nil

    // Initialize KinesteXAIKit with your credentials
    let kinestex = KinesteXAIKit(
        apiKey: "YOUR_API_KEY",
        companyName: "YOUR_COMPANY_NAME",
        userId: "YOUR_USER_ID"
    )

    // Define custom workout exercises
    let exercises = [
        WorkoutSequenceExercise(
            exerciseId: "jz73VFlUyZ9nyd64OjRb",
            reps: 15,
            duration: nil,
            includeRestPeriod: true,
            restDuration: 20
        ),
        WorkoutSequenceExercise(
            exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
            reps: 10,
            duration: 30,
            includeRestPeriod: true,
            restDuration: 15
        ),
        WorkoutSequenceExercise(
            exerciseId: "gJGOiZhCvJrhEP7sTy78",
            reps: 20,
            duration: nil,
            includeRestPeriod: false,
            restDuration: 0
        )
    ]

    var body: some View {
        VStack {
            Text("Custom Workout")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showKinesteX.toggle()
            }) {
                Text("Start Custom Workout")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.green.cornerRadius(10))
                    .padding(.horizontal)
            }

            Spacer()
        }
        .fullScreenCover(isPresented: $showKinesteX) {
            kinestex.createCustomWorkoutView(
                exercises: exercises,
                user: nil,
                style: nil,
                isLoading: $isLoading,
                workoutAction: $workoutAction,
                onMessageReceived: { message in
                    switch message {
                    case .custom_type(let value):
                        guard let type = value["type"] as? String else { return }
                        if type == "all_resources_loaded" {
                            allResourcesLoaded = true
                            // Start workout when resources are ready
                            workoutAction = ["workout_activity_action": "start"]
                        }
                    case .exit_kinestex(_):
                        showKinesteX = false
                        allResourcesLoaded = false
                    case .workout_overview(let data):
                        print("Workout overview: \(data)")
                    case .error_occurred(let data):
                        print("Error: \(data)")
                    default:
                        print("Message received: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    CustomWorkoutView()
}
```

_React Native_
```jsx
import { StyleSheet, View, Button, Text } from "react-native";
import { useEffect, useRef, useState } from "react";
import { Camera } from "expo-camera";
import { SafeAreaView } from "react-native-safe-area-context";
import KinestexSDK from "kinestex-sdk-react-native";
import {
  KinesteXSDKCamera,
  WorkoutSequenceExercise,
  IPostData,
  IntegrationOption
} from "kinestex-sdk-react-native/src/types";

export default function CustomWorkoutScreen() {
  const kinestexSDKRef = useRef<KinesteXSDKCamera>(null);
  const [permission, setPermission] = useState(false);
  const [allResourcesLoaded, setAllResourcesLoaded] = useState(false);
  const [showKinestex, setShowKinestex] = useState(true);

  // Define workout sequence of exercises
  const customWorkoutExercises: WorkoutSequenceExercise[] = [
    {
      exerciseId: "jz73VFlUyZ9nyd64OjRb",
      reps: 15,
      duration: null, // unlimited time to complete reps
      includeRestPeriod: true,
      restDuration: 20,
    },
    {
      exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
      reps: 10,
      duration: 30,
      includeRestPeriod: true,
      restDuration: 15,
    },
    // Duplicate to create a set
    {
      exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
      reps: 10,
      duration: 30,
      includeRestPeriod: true,
      restDuration: 15,
    },
    {
      exerciseId: "gJGOiZhCvJrhEP7sTy78",
      reps: 20,
      duration: null,
      includeRestPeriod: false,
      restDuration: 0,
    },
  ];

  // Configuration data with customWorkoutExercises
  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    company: 'YOUR_COMPANY_NAME',
    userId: "user-123",
    customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
    style: {
      style: "dark",
    },
  };

  // Request camera permission
  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestCameraPermissionsAsync();
      if (status === "granted") {
        setPermission(true);
      }
    })();
  }, []);

  // Handle messages from SDK
  const handleMessage = (type: string, data: { [key: string]: any }) => {
    switch (type) {
      case "exit_kinestex":
        console.log("User wishes to exit");
        setAllResourcesLoaded(false);
        break;
      case "workout_exit_request":
        console.log("Workout exit request:", data);
        setAllResourcesLoaded(false);
        setShowKinestex(false);
        break;
      case "all_resources_loaded":
        console.log("All resources loaded");
        setAllResourcesLoaded(true);
        // Start the workout
        kinestexSDKRef.current?.sendAction("workout_activity_action", "start");
        break;
      case "workout_overview":
        console.log("Workout overview:", data);
        break;
      case "error_occurred":
        console.log("Error:", data);
        break;
      default:
        console.log("Message:", type, data);
        break;
    }
  };

  if (!permission) {
    return (
      <View style={styles.launchContainer}>
        <Text style={styles.statusText}>Camera permission required</Text>
      </View>
    );
  }

  return (
    <SafeAreaView style={styles.safeArea}>
      <Text style={styles.statusText}>
        {!showKinestex
          ? "KinesteX is not activated"
          : allResourcesLoaded
          ? "All resources loaded"
          : "KinesteX is loading in background"}
      </Text>
      <View
        style={[
          styles.sdkContainer,
          showKinestex && !allResourcesLoaded && styles.hiddenSdkContainer,
        ]}
      >
        {showKinestex ? (
          <KinestexSDK
            ref={kinestexSDKRef}
            data={postData}
            integrationOption={IntegrationOption.CUSTOM_WORKOUT}
            handleMessage={handleMessage}
          />
        ) : (
          <View style={styles.launchContainer}>
            <Button
              title="Show Kinestex Again"
              onPress={() => setShowKinestex(true)}
            />
          </View>
        )}
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: "black",
  },
  statusText: {
    padding: 8,
    textAlign: "center",
    color: "white",
  },
  launchContainer: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "black",
  },
  sdkContainer: {
    flex: 1,
  },
  hiddenSdkContainer: {
    width: 0,
    height: 0,
    overflow: "hidden",
  },
});
```

_React (TypeScript)_
```tsx
import React, { useRef, useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

// Define workout sequence exercise type
interface WorkoutSequenceExercise {
  exerciseId: string;
  reps: number | null;
  duration: number | null;
  includeRestPeriod: boolean;
  restDuration: number;
}

const CustomWorkoutScreen: React.FC = () => {
  const ref = useRef<KinesteXSDKCamera>(null);
  const [allResourcesLoaded, setAllResourcesLoaded] = useState(false);
  const [showKinestex, setShowKinestex] = useState(true);

  const customWorkoutExercises: WorkoutSequenceExercise[] = [
    {
      exerciseId: "jz73VFlUyZ9nyd64OjRb",
      reps: 15,
      duration: null,
      includeRestPeriod: true,
      restDuration: 20,
    },
    {
      exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
      reps: 10,
      duration: 30,
      includeRestPeriod: true,
      restDuration: 15,
    },
  ];

  // Include customWorkoutExercises in postData
  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    company: 'YOUR_COMPANY_NAME',
    userId: "user-123",
    customWorkoutExercises: customWorkoutExercises,
    style: { style: "dark" },
  };

  const handleMessage = (type: string, data: Record<string, unknown>) => {
    switch (type) {
      case "all_resources_loaded":
        setAllResourcesLoaded(true);
        ref.current?.sendAction("workout_activity_action", "start");
        break;
      case "workout_exit_request":
      case "exit_kinestex":
        setAllResourcesLoaded(false);
        setShowKinestex(false);
        break;
    }
  };

  return (
    <div style={{ height: '100vh', width: '100vw', backgroundColor: 'black' }}>
      <div style={{
        display: showKinestex && !allResourcesLoaded ? 'none' : 'block',
        height: '100%',
        width: '100%',
      }}>
        {showKinestex ? (
          <KinesteXSDK
            ref={ref}
            data={postData}
            integrationOption={IntegrationOption.CUSTOM_WORKOUT}
            handleMessage={handleMessage}
          />
        ) : (
          <button onClick={() => setShowKinestex(true)}>
            Show Kinestex Again
          </button>
        )}
      </div>
    </div>
  );
};

export default CustomWorkoutScreen;
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Custom Workout',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // The KinesteX view is ALWAYS mounted. We only toggle visibility.
  final ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);

  // Signals when SDK reports all resources loaded.
  final ValueNotifier<bool> allResourcesLoaded = ValueNotifier<bool>(false);

  // Optional: SDK loading notifier.
  final ValueNotifier<bool> sdkLoading = ValueNotifier<bool>(false);

  bool permissionGranted = false;

  late final List<WorkoutSequenceExercise> customWorkoutExercises;

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();

    customWorkoutExercises = const [
      WorkoutSequenceExercise(
        exerciseId: "jz73VFlUyZ9nyd64OjRb",
        reps: 15,
        duration: null,
        includeRestPeriod: true,
        restDuration: 20,
      ),
      WorkoutSequenceExercise(
        exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
        reps: 10,
        duration: 30,
        includeRestPeriod: true,
        restDuration: 15,
      ),
      // Duplicate to create a set
      WorkoutSequenceExercise(
        exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
        reps: 10,
        duration: 30,
        includeRestPeriod: true,
        restDuration: 15,
      ),
      WorkoutSequenceExercise(
        exerciseId: "gJGOiZhCvJrhEP7sTy78",
        reps: 20,
        duration: null,
        includeRestPeriod: false,
        restDuration: 0,
      ),
    ];
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      setState(() => permissionGranted = false);
      _showCameraAccessDeniedAlert();
    } else {
      setState(() => permissionGranted = true);
    }
  }

  void _showCameraAccessDeniedAlert() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: const Text("Camera Permission Denied"),
            content: const Text(
              "Camera access is required for this app to function properly.",
            ),
            actions: <Widget>[
              TextButton(
                child: const Text("OK"),
                onPressed: () => Navigator.of(context).pop(),
              ),
            ],
          );
        },
      );
    });
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        allResourcesLoaded.value = false;
        showKinesteX.value = false;
      });
      return;
    }

    try {
      final data = message.data;
      final type = data['type'];

      switch (type) {
        case 'all_resources_loaded':
          allResourcesLoaded.value = true;
          showKinesteX.value = true;
          KinesteXAIFramework.sendAction(
            "workout_activity_action",
            "start",
          );
          break;

        case 'workout_exit_request':
          allResourcesLoaded.value = false;
          showKinesteX.value = false;
          break;

        case 'error_occurred':
          final errorMsg = data['message']?.toString() ?? 'Unknown error';
          print('Error from KinesteX SDK: $errorMsg');
          break;

        default:
          break;
      }
    } catch (_) {
      allResourcesLoaded.value = false;
      showKinesteX.value = false;
    }
  }

  Widget createCustomWorkoutView() {
    return Center(
      child: KinesteXAIFramework.createCustomWorkoutView(
        customWorkouts: customWorkoutExercises,
        isShowKinestex: showKinesteX,
        isLoading: sdkLoading,
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  Widget buildCornerIndicator() {
    return SafeArea(
      child: Padding(
        padding: const EdgeInsets.only(top: 8, right: 8),
        child: ValueListenableBuilder<bool>(
          valueListenable: allResourcesLoaded,
          builder: (context, loaded, _) {
            return ValueListenableBuilder<bool>(
              valueListenable: showKinesteX,
              builder: (context, visible, __) {
                Color bg = Colors.black.withOpacity(0.75);
                IconData icon = Icons.hourglass_bottom;
                String label = 'KinesteX loading...';

                if (!permissionGranted) {
                  bg = Colors.red.withOpacity(0.85);
                  icon = Icons.videocam_off;
                  label = 'Camera permission required';
                } else if (!loaded) {
                  bg = Colors.orange.withOpacity(0.85);
                  icon = Icons.downloading;
                  label = 'Loading in background';
                } else if (loaded && visible) {
                  bg = Colors.green.withOpacity(0.85);
                  icon = Icons.check_circle;
                  label = 'All resources loaded';
                } else if (loaded && !visible) {
                  bg = Colors.grey.withOpacity(0.85);
                  icon = Icons.visibility_off;
                  label = 'KinesteX hidden';
                }

                return Container(
                  constraints: const BoxConstraints(maxWidth: 260),
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 10,
                  ),
                  decoration: BoxDecoration(
                    color: bg,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Icon(icon, color: Colors.white, size: 18),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          label,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 13,
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                      ),
                      if (!permissionGranted)
                        TextButton(
                          onPressed: _checkCameraPermission,
                          child: const Text(
                            'Grant',
                            style: TextStyle(color: Colors.white),
                          ),
                        )
                      else if (loaded && !visible)
                        TextButton(
                          onPressed: () {
                            showKinesteX.value = true;
                            KinesteXAIFramework.sendAction(
                              "workout_activity_action",
                              "start",
                            );
                          },
                          child: const Text(
                            'Show',
                            style: TextStyle(color: Colors.white),
                          ),
                        )
                      else if (!loaded)
                        const SizedBox(
                          height: 16,
                          width: 16,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Colors.white,
                          ),
                        ),
                    ],
                  ),
                );
              },
            );
          },
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          ValueListenableBuilder<bool>(
            valueListenable: showKinesteX,
            builder: (context, isVisible, _) {
              return IgnorePointer(
                ignoring: !isVisible,
                child: AnimatedOpacity(
                  duration: const Duration(milliseconds: 250),
                  opacity: isVisible ? 1.0 : 0.0,
                  child: createCustomWorkoutView(),
                ),
              );
            },
          ),
          Positioned(top: 0, right: 0, child: buildCornerIndicator()),
        ],
      ),
    );
  }
}
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: Custom Workout</title>
    <style>
      body { margin: 0; font-family: Arial, sans-serif; }
      #statusText { padding: 8px; text-align: center; background: #333; color: white; }
      #launchButton { display: block; margin: 20px auto; padding: 15px 30px; font-size: 16px; cursor: pointer; }
    </style>
  </head>
  <body>
    <div id="statusText">Click button to start custom workout</div>
    <button id="launchButton">Start Custom Workout</button>
    <div id="webViewContainer" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none; background: black;">
      <iframe id="webView" frameborder="0" allow="camera; autoplay; accelerometer; gyroscope; magnetometer" sandbox="allow-same-origin allow-scripts" style="width: 100%; height: 100%"></iframe>
    </div>

    <script>
      // Define custom workout exercises
      const customWorkoutExercises = [
        {
          exerciseId: "jz73VFlUyZ9nyd64OjRb",
          reps: 15,
          duration: null, // unlimited time to complete reps
          includeRestPeriod: true,
          restDuration: 20,
        },
        {
          exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
          reps: 10,
          duration: 30,
          includeRestPeriod: true,
          restDuration: 15,
        },
        {
          exerciseId: "gJGOiZhCvJrhEP7sTy78",
          reps: 20,
          duration: null,
          includeRestPeriod: false,
          restDuration: 0,
        },
      ];

      // Configuration data
      const postData = {
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY",
        key: "YOUR_API_KEY",
        style: "dark",
        customWorkoutExercises: customWorkoutExercises,
      };

      // IMPORTANT: This is the URL for the Custom Workout integration
      const srcURL = "https://ai.kinestex.com/custom-workout";
      let allResourcesLoaded = false;

      // Get references to iframe and container at the top
      const webView = document.getElementById("webView");
      const webViewContainer = document.getElementById("webViewContainer");

      // Send configuration data to the iframe via postMessage
      // Includes retry logic for Apple Safari compatibility
      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      // Send action to the SDK (e.g., start workout)
      const sendAction = (action, value) => {
        const messagePayload = { [action]: String(value) };
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(messagePayload, srcURL);
        }
      };

      document.getElementById("launchButton").addEventListener("click", function () {
        webViewContainer.style.display = "block";
        document.getElementById("statusText").textContent = "Loading custom workout...";
        webView.src = srcURL;
        webView.onload = sendMessage;
      });

      window.addEventListener("message", function (event) {
        if (event.origin !== "https://ai.kinestex.com") return;

        let message;
        try {
          message = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
        } catch {
          return;
        }

        const statusText = document.getElementById("statusText");

        switch (message.type) {
          case "kinestex_launched":
            console.log("KinesteX launched");
            break;
          case "kinestex_loaded":
            console.log("KinesteX loaded, sending post data");
            sendMessage();
            break;
          case "all_resources_loaded":
            allResourcesLoaded = true;
            statusText.textContent = "All resources loaded - starting workout";
            // Start the workout after resources are loaded
            sendAction("workout_activity_action", "start");
            break;
          case "workout_overview":
            console.log("Workout overview:", message);
            break;
          case "workout_exit_request":
          case "exit_kinestex":
            allResourcesLoaded = false;
            webViewContainer.style.display = "none";
            statusText.textContent = "Workout ended. Click button to restart.";
            console.log("User exited the custom workout");
            break;
          case "error_occurred":
            console.error("Error:", message);
            statusText.textContent = "Error occurred: " + (message.message || "Unknown error");
            break;
          default:
            console.log("Message received:", message.type, message);
            break;
        }
      });
    </script>
  </body>
</html>
```

### Camera Component

KinesteX Motion Recognition: Real-Time Engagement.

- **Interactive Tracking**: Advanced motion recognition for immersive fitness experiences
- **Real-Time Feedback**: Instantly track reps, spot mistakes, and calculate calories burned
- **Boost Motivation**: Keep users engaged with detailed exercise feedback
- **Custom Integration**: Adapt camera placement to fit your app's design

**Important — what to pass for `currentExercise` and `exercises`:** we **recommend exercise IDs** — they're stable, human-readable, and you already have them when listing exercises from the Content API. Set `exerciseFetchType: "exercise_id"` to use them. The Camera Component also accepts **model IDs** (the default, kept for backward compatibility — but they require an extra round-trip through the Content API to look up) and **exercise titles** (case-sensitive — convenient for prototyping, but title matching depends on locale-normalization and can mismatch similar exercises, so prefer IDs in production). See the **Fetching Exercises** section below.

**Before showing the camera UI**, wait for both `model_warmedup` and `models_loaded` events to fire. See the **Preloading & Events** section below.

**Quick Start**

_Swift (iOS)_
```swift
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
@State var currentExercise = "3"

kinestex.createCameraView(
    exercises: ["3"], // preload every model ID you may switch to
    currentExercise: $currentExercise,
    user: nil,
    isLoading: $isLoading,
    onMessageReceived: { message in
        switch message {
        case .reps(let value):
            reps = value["value"] as? Int ?? 0
        case .mistake(let value):
            mistake = value["value"] as? String ?? "--"
        default:
            break
        }
    }
)
```

_Kotlin (Android)_
```kotlin
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
val cameraView = KinesteXSDK.createCameraComponent(
    context = this,
    currentExercise = "3",
    exercises = listOf("3"),
    user = userDetails,
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        when (message) {
            is WebViewMessage.Reps ->
                (message.data["value"] as? Int)?.let { reps = it }
            is WebViewMessage.Mistake ->
                (message.data["value"] as? String)?.let { mistake = it }
            else -> {}
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
// postData seeds the SDK with INITIAL values. To change them at runtime,
// call methods on kinestexSDKRef (e.g. changeExercise) — see "Camera: Controls".
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: '3',
  exercises: ['3'], // preload every model ID you may switch to
  style: { style: 'dark' },
};

<KinestexSDK
  ref={kinestexSDKRef}
  data={postData}
  integrationOption={IntegrationOption.CAMERA}
  handleMessage={(type, data) => {
    if (type === 'successful_repeat') setReps(data.value);
    if (type === 'mistake') setMistake(data.value);
  }}
/>
```

_Flutter_
```dart
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
KinesteXAIFramework.createCameraComponent(
  isShowKinestex: showKinesteX,
  exercises: ["3"],
  currentExercise: "3",
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    if (message is Reps) {
      setState(() => reps = message.data['value']);
    }
    if (message is Mistake) {
      setState(() => mistake = message.data['value']);
    }
  },
)
```

_HTML / JavaScript_
```html
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
const postData = {
  // ... your initial fields (key, userId, company, etc.)
  currentExercise: "3",
  exercises: ["3"],
};

const srcURL = "https://ai.kinestex.com/camera";
webView.src = srcURL;
webView.onload = () => sendMessage(postData);

window.addEventListener("message", (event) => {
  if (event.origin !== "https://ai.kinestex.com") return;
  const msg = JSON.parse(event.data);
  if (msg.type === 'successful_repeat') console.log('Rep:', msg.value);
  if (msg.type === 'mistake') console.log('Mistake:', msg.data?.value);
});
```

_React (TypeScript)_
```tsx
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
import { useRef } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

const ref = useRef<KinesteXSDKCamera>(null);

// postData seeds the SDK with INITIAL values. To change them at runtime,
// call methods on the ref (e.g. ref.current?.changeExercise) — see "Camera: Controls".
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: '3',
  exercises: ['3'],
  style: { style: 'dark' },
};

<KinesteXSDK
  ref={ref}
  data={postData}
  integrationOption={IntegrationOption.CAMERA}
  handleMessage={(type, data) => {
    if (type === 'successful_repeat') setReps(data.value as number);
    if (type === 'mistake') setMistake(data.value as string);
  }}
/>
```

#### Model IDs

> **Recommendation:** prefer **exercise IDs** — they're easier to get (you already have them when listing exercises from the Content API) and don't require an extra round-trip just to look up a numeric model ID. Set `exerciseFetchType: "exercise_id"` and pass `exercise.id` directly. See the [Fetching Exercises](#camera-component-fetching-exercises) section.

Model IDs are still supported (it's the default when `exerciseFetchType` is omitted) and useful when you already have one handy — e.g. from a stored `WorkoutModel.sequence` or the admin dashboard. They are numeric values like `"3"` for Squats or `"394"` for Jumping Jack.

**Three ways to get a model ID:**

1. **Content API** — call `fetchExercises()` (Swift / Kotlin), `fetchContent(contentType: ContentType.exercise)` (Flutter), or `GET /api/v1/exercises` (REST). Each `ExerciseModel` has a `model_id` field. See the [Content API](/docs/content-api) section for full details. *(Note: this is the extra round-trip that exercise IDs let you skip.)*
2. **Admin dashboard** — open the exercise in [admin.kinestex.com](https://admin.kinestex.com); the model ID is shown at the top of the page header.
3. **Workout sequences** — when iterating `WorkoutModel.sequence`, each `ExerciseModel` entry exposes its own `model_id`.

**Fetch a Model ID**

_Swift (iOS)_
```swift
Task {
    let result = await kinestex.fetchExercises(limit: 10)
    if case .success(let response) = result,
       let exercise = response.exercises.first {
        // exercise.modelId is what the Camera Component expects
        currentExercise = exercise.modelId
    }
}
```

_Kotlin (Android)_
```kotlin
lifecycleScope.launch {
    val result = withContext(Dispatchers.IO) {
        KinesteXSDK.api.fetchAPIContentData(
            contentType = ContentType.EXERCISE,
            limit = 10
        )
    }
    if (result is APIContentResult.Exercises) {
        // exercise.modelId is what the Camera Component expects
        currentExercise = result.exercises.firstOrNull()?.modelId ?: ""
    }
}
```

_React Native_
```jsx
const res = await fetch(
  'https://admin.kinestex.com/api/v1/exercises?limit=10',
  { headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json();
// exercise.model_id is what the Camera Component expects.
// Pass it as initial value, or switch later via ref.current?.changeExercise(...).
const modelId = exercises[0].model_id;
```

_Flutter_
```dart
final result = await KinesteXAIFramework.apiService.fetchContent(
  contentType: ContentType.exercise,
  limit: 10,
);
// exercise.modelId is what the Camera Component expects
if (result is ExercisesResult) {
  final modelId = result.response.exercises.first.modelId;
}
```

_HTML / JavaScript_
```html
const res = await fetch(
  'https://admin.kinestex.com/api/v1/exercises?limit=10',
  { headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json();
// exercise.model_id is what the Camera Component expects
const modelId = exercises[0].model_id;
```

_React (TypeScript)_
```tsx
const res = await fetch(
  'https://admin.kinestex.com/api/v1/exercises?limit=10',
  { headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json() as { exercises: ExerciseModel[] };
// exercise.model_id is what the Camera Component expects
const modelId = exercises[0].model_id;
```

#### Fetching Exercises (by ID or Title)

Besides model IDs, the Camera Component can fetch exercises by **exercise ID** (recommended) or **exercise title**. Set the `exerciseFetchType` parameter to choose the form:

| `exerciseFetchType` | Meaning | What goes in `exercises` / `currentExercise` |
|---|---|---|
| `"exercise_id"` ✅ **recommended** | Exercise IDs from the Content API — easiest to use, no extra round-trip | e.g. `"squats_v2"` |
| `"model_id"` *(default)* | Numeric model IDs — kept for backward compatibility; requires a Content API lookup to obtain | `"3"`, `"394"` |
| `"exercise_title"` | Exercise titles (case-sensitive) — handy for prototyping, but title matching depends on locale-normalization and can mismatch similar exercises | `"Squats"`, `"Jumping Jack"` |

Omitting `exerciseFetchType` keeps the default model-ID behavior — no migration needed for existing integrations.

**Where to pass it:**
- **React Native (SDK v1.3.1+):** directly in `postData`, alongside `exercises` and `currentExercise`.
- **Swift, Kotlin, Flutter, HTML/JS, React (TypeScript):** inside `customParams` / `customParameters` along with `exercises` and `currentExercise`.

**Keep one form per session:** use the same form for both `exercises` and `currentExercise`, and for any later switches.

**Fetch by Exercise Title**

_Swift (iOS)_
```swift
// exerciseFetchType goes inside customParams
kinestex.createCameraView(
    exercises: ["Squats", "Jumping Jack"],
    currentExercise: $currentExercise, // e.g. "Squats"
    customParams: [
        "exerciseFetchType": "exercise_title" // "model_id" (default) | "exercise_id" | "exercise_title"
    ]
)
```

_Kotlin (Android)_
```kotlin
// exerciseFetchType goes inside customParams
KinesteXSDK.createCameraComponent(
    context = this,
    currentExercise = "Squats",
    exercises = listOf("Squats", "Jumping Jack"),
    customParams = mutableMapOf(
        "exerciseFetchType" to "exercise_title" // "model_id" (default) | "exercise_id" | "exercise_title"
    ),
    permissionHandler = this
)
```

_React Native_
```jsx
// React Native v1.3.1+: exerciseFetchType is direct in postData
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: 'Squats',
  exercises: ['Squats', 'Jumping Jack'],
  exerciseFetchType: 'exercise_title', // "model_id" (default) | "exercise_id" | "exercise_title"
};
```

_Flutter_
```dart
// exerciseFetchType goes inside customParams
KinesteXAIFramework.createCameraComponent(
  isShowKinestex: showKinesteX,
  exercises: ["Squats", "Jumping Jack"],
  currentExercise: "Squats",
  customParams: {
    "exerciseFetchType": "exercise_title", // "model_id" (default) | "exercise_id" | "exercise_title"
  },
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: handleWebViewMessage,
)
```

_HTML / JavaScript_
```html
// exerciseFetchType goes inside customParams
const postData = {
  // ... your initial fields (key, userId, company, etc.)
  currentExercise: "Squats",
  exercises: ["Squats", "Jumping Jack"],
  customParams: {
    exerciseFetchType: "exercise_title", // "model_id" (default) | "exercise_id" | "exercise_title"
  },
};
```

_React (TypeScript)_
```tsx
// exerciseFetchType goes inside customParameters
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  currentExercise: 'Squats',
  exercises: ['Squats', 'Jumping Jack'],
  customParameters: {
    exerciseFetchType: 'exercise_title', // "model_id" (default) | "exercise_id" | "exercise_title"
  },
};
```

#### Loading More Exercises at Runtime (React Native v1.3.1+)

**Availability:** React Native SDK `v1.3.1+` only. Other platforms must pass the full `exercises` list at initialization.

In React Native you can fetch and cache **additional** exercise models **after the session has started**, without re-mounting the camera. This is done by calling `sendAction` with the `"load_models"` action and an `extras` object containing the new identifiers.

**Three-step flow:**

1. Send the `load_models` action with the identifiers you want to add.
2. Wait for the `models_loaded` event — `data.modelIds` echoes the identifiers that resolved.
3. Switch the active exercise with `changeExercise(...)` (do **not** repeat `exerciseFetchType`).

If `exercises` is missing or empty, the SDK posts back `{ "type": "error_occurred", "message": "load_models: no exercises provided" }`. Per-identifier failures arrive as separate `error_occurred` messages; any identifiers that did resolve are still listed in `modelIds` and are switchable.

**Load More Models, Then Switch**

_React Native_
```jsx
// Step 1 — fetch additional models at runtime
sdkRef.current?.sendAction(
  "workout_activity_action",
  "load_models",
  {
    exercises: ["Jumping Jack", "Lunges"],
    exerciseFetchType: "exercise_title", // match the form you used initially
  }
);

// Step 2 — wait for models_loaded, then switch
const handleMessage = (type: string, data: { [key: string]: any }) => {
  if (type === "models_loaded" && data.modelIds?.includes("Jumping Jack")) {
    // Step 3 — switch the active exercise. Do NOT repeat exerciseFetchType here.
    sdkRef.current?.changeExercise("Jumping Jack");
  }
  if (type === "error_occurred") {
    console.warn("KinesteX error:", data.message);
  }
};
```

#### Preloading & Events

Two events fire as the component initializes. Wait for **both** before revealing the camera UI to the user.

| Event | Meaning |
|---|---|
| `model_warmedup` | Pose-tracking (MediaPipe) model is ready |
| `models_loaded` | All exercise models in `exercises` have downloaded |

**Pattern:** mount the camera hidden (e.g. `opacity: 0`) with a loader on top. Reveal once both events have fired.

**Wait for Both Events**

_Swift (iOS)_
```swift
@State private var modelWarmedUp = false
@State private var modelsLoaded = false
var isReady: Bool { modelWarmedUp && modelsLoaded }

kinestex.createCameraView(
    exercises: ["3"],
    currentExercise: $currentExercise,
    user: nil,
    isLoading: $isLoading,
    onMessageReceived: { message in
        if case .custom_type(let value) = message,
           let type = value["type"] as? String {
            if type == "model_warmedup" { modelWarmedUp = true }
            if type == "models_loaded"  { modelsLoaded  = true }
        }
    }
)
.opacity(isReady ? 1 : 0)
```

_Kotlin (Android)_
```kotlin
var modelWarmedUp = false
var modelsLoaded = false

val cameraView = KinesteXSDK.createCameraComponent(
    context = this,
    currentExercise = "3",
    exercises = listOf("3"),
    user = null,
    isLoading = viewModel.isLoading,
    onMessageReceived = { message ->
        if (message is WebViewMessage.CustomType) {
            when (message.data["type"] as? String) {
                "model_warmedup" -> modelWarmedUp = true
                "models_loaded"  -> modelsLoaded  = true
            }
            cameraView.alpha = if (modelWarmedUp && modelsLoaded) 1f else 0f
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
const [modelWarmedUp, setModelWarmedUp] = useState(false);
const [modelsLoaded, setModelsLoaded] = useState(false);
const isReady = modelWarmedUp && modelsLoaded;

<View style={{ opacity: isReady ? 1 : 0 }}>
  <KinestexSDK
    data={postData}
    integrationOption={IntegrationOption.CAMERA}
    handleMessage={(type) => {
      if (type === 'model_warmedup') setModelWarmedUp(true);
      if (type === 'models_loaded')  setModelsLoaded(true);
    }}
  />
</View>
```

_Flutter_
```dart
bool modelWarmedUp = false;
bool modelsLoaded  = false;

KinesteXAIFramework.createCameraComponent(
  isShowKinestex: showKinesteX,
  exercises: ["3"],
  currentExercise: "3",
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    // Model-readiness events arrive as CustomType; the name is in data['type']
    if (message is CustomType) {
      if (message.data['type'] == 'model_warmedup') {
        setState(() => modelWarmedUp = true);
      }
      if (message.data['type'] == 'models_loaded') {
        setState(() => modelsLoaded = true);
      }
    }
  },
)
```

_HTML / JavaScript_
```html
let modelWarmedUp = false, modelsLoaded = false;
const reveal = () => {
  if (modelWarmedUp && modelsLoaded) iframe.style.opacity = '1';
};

iframe.style.opacity = '0';
window.addEventListener("message", (event) => {
  if (event.origin !== "https://ai.kinestex.com") return;
  const msg = JSON.parse(event.data);
  if (msg.type === 'model_warmedup') { modelWarmedUp = true; reveal(); }
  if (msg.type === 'models_loaded')  { modelsLoaded  = true; reveal(); }
});
```

_React (TypeScript)_
```tsx
const [modelWarmedUp, setModelWarmedUp] = useState(false);
const [modelsLoaded, setModelsLoaded] = useState(false);
const isReady = modelWarmedUp && modelsLoaded;

<div style={{ opacity: isReady ? 1 : 0 }}>
  <KinesteXSDK
    data={postData}
    integrationOption={IntegrationOption.CAMERA}
    handleMessage={(type) => {
      if (type === 'model_warmedup') setModelWarmedUp(true);
      if (type === 'models_loaded')  setModelsLoaded(true);
    }}
  />
</div>
```

#### Controls

**Switching exercises in real time.** Set `currentExercise` to any model ID from the `exercises` array — the camera component swaps tracking immediately.

**Control commands.** Send any of these strings as `currentExercise` to control the session:

| Command | Effect |
|---|---|
| `"Pause Exercise"` | Pauses motion tracking; rep counter freezes |
| `"Pause Audio"` | Mutes voice feedback |
| `"Resume Audio"` | Re-enables voice feedback |
| `"Workout Overview"` | Triggers a summary snapshot for the current session |
| `"Stop Camera"` | ⚠️ **Destructive** — releases camera + models, fires `stop_camera`. Not recoverable without re-mounting the component (see warning below) |

To resume tracking after a pause, set `currentExercise` back to a real model ID from `exercises`.

> ⚠️ **`"Stop Camera"` is destructive — avoid it for normal flows.** It tears down the camera, MediaPipe, and all loaded exercise models, then fires the `stop_camera` event. **You cannot recover from it without unmounting and re-creating the entire camera component.** Only send it when the user is permanently leaving the camera screen. For temporary pauses, use `"Pause Exercise"` instead.

**Switch Exercise and Send Control Commands**

_Swift (iOS)_
```swift
// Switch exercise (Swift uses two-way binding via @State)
currentExercise = "394"

// Pause / resume tracking
currentExercise = "Pause Exercise"
currentExercise = "3"
```

_Kotlin (Android)_
```kotlin
// Switch exercise
KinesteXSDK.updateCurrentExercise("394")

// Pause / resume tracking
KinesteXSDK.updateCurrentExercise("Pause Exercise")
KinesteXSDK.updateCurrentExercise("3")
```

_React Native_
```jsx
// Switch exercise
kinestexSDKRef.current?.changeExercise("394");

// Pause / resume tracking
kinestexSDKRef.current?.changeExercise("Pause Exercise");
kinestexSDKRef.current?.changeExercise("3");
```

_Flutter_
```dart
// All controls go through your updateExercise ValueNotifier
updateExercise.value = "394";
updateExercise.value = "Pause Exercise";
updateExercise.value = "3";
```

_HTML / JavaScript_
```html
// Switch exercise
webView.contentWindow.postMessage(
  { currentExercise: "394" },
  srcURL
);

// Pause / resume tracking
webView.contentWindow.postMessage({ currentExercise: "Pause Exercise" }, srcURL);
webView.contentWindow.postMessage({ currentExercise: "3" }, srcURL);
```

_React (TypeScript)_
```tsx
// Switch exercise
ref.current?.changeExercise("394");

// Pause / resume tracking
ref.current?.changeExercise("Pause Exercise");
ref.current?.changeExercise("3");
```

#### Customization

Pass any of these fields in `customParams` (Swift / Kotlin / Flutter) or directly in `postData` (HTML/JS, React Native, React TS) at initialization.

| Field | Type | Description |
|---|---|---|
| `restSpeeches` | `string[]` | Audio phrases to preload (from `ExerciseModel.rest_speech`) |
| `videoURL` | `string` | Use a video file instead of the live camera |
| `landmarkColor` | `string` | Pose overlay color in hex with `#` (default `#14FF00`) |
| `showSilhouette` | `boolean` | Show "get into frame" silhouette (default `true`) |
| `includeRealtimeAccuracy` | `boolean` | (Beta) Stream live position-confidence alongside reps |
| `includePoseData` | `string[]` | Any of `"angles"`, `"poseLandmarks"`, `"worldLandmarks"`. **Performance impact** — only enable for custom calculations |

#### Event Reference

All events the Camera Component emits to the host app:

| Event | Payload | When |
|---|---|---|
| `model_warmedup` | `{ message }` | Pose model is ready |
| `models_loaded` | `{ message }` | All exercise models in `exercises` finished downloading |
| `person_in_frame` | `{ message }` | User entered the silhouette frame |
| `successful_repeat` | `{ exercise, value, accuracy }` | A rep was counted (`value` = total reps so far) |
| `mistake` | `{ value }` | Form mistake detected |
| `correct_position_accuracy` | `{ accuracy }` | (Beta) Live position confidence — only when `includeRealtimeAccuracy: true` |
| `pose_landmarks` | `{ poseLandmarks }` | Per-frame screen-space landmarks — only when `includePoseData` includes `"poseLandmarks"` |
| `world_landmarks` | `{ worldLandmarks }` | Per-frame world-space landmarks — only when `includePoseData` includes `"worldLandmarks"` |
| `speech_fetch_complete` | `{ successCount, failureCount }` | All `restSpeeches` finished loading |
| `error_occurred` | `{ message }` or `{ data, error }` | Any error (model fetch fail, phrase fail, etc.) |
| `warning` | `{ data }` | Non-fatal config issue (e.g. no model IDs provided) |
| `stop_camera` | `{ message }` | Confirms the `"Stop Camera"` command finished |

#### Pose Data

When `includePoseData` contains `"poseLandmarks"` or `"worldLandmarks"`, the camera emits per-frame events with raw skeleton data. **Only enable this if you're doing custom calculations — there is a performance cost.**

**Two coordinate spaces:**

- `poseLandmarks` — values 0–1, normalized to the camera frame.
- `worldLandmarks` — meters, relative to the hips (best Z accuracy).

Each landmark has `{ x, y, z, visibility }` (all 0–1).

**Available landmarks** (same names in both spaces): `nose`, `leftEyeInner`, `leftEye`, `leftEyeOuter`, `rightEyeInner`, `rightEye`, `rightEyeOuter`, `leftEar`, `rightEar`, `mouthLeft`, `mouthRight`, `leftShoulder`, `rightShoulder`, `leftElbow`, `rightElbow`, `leftWrist`, `rightWrist`, `leftPinky`, `rightPinky`, `leftIndex`, `rightIndex`, `leftThumb`, `rightThumb`, `leftHip`, `rightHip`, `leftKnee`, `rightKnee`, `leftAnkle`, `rightAnkle`, `leftHeel`, `rightHeel`, `leftFootIndex`, `rightFootIndex`.

**Available angles** (when `"angles"` is included — both 2D and 3D versions are emitted): `leftKneeAngle`, `rightKneeAngle`, `leftHipAngle`, `rightHipAngle`, `leftShoulderAngle`, `rightShoulderAngle`, `leftElbowAngle`, `rightElbowAngle`, `leftWristAngle`, `rightWristAngle`, `leftAnkleAngle`, `rightAnkleAngle`, `leftArmpitAngle`, `rightArmpitAngle`.

#### Complete Example

Minimal working example with **Next** / **Previous** buttons that cycle between exercises and a live rep counter in the UI.

**Complete Implementation**

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct CameraScreen: View {
    let kinestex = KinesteXAIKit(
        apiKey: "YOUR_API_KEY",
        companyName: "YOUR_COMPANY_NAME",
        userId: "YOUR_USER_ID"
    )

    // 3 = Squats, 394 = Jumping Jack
    let exerciseIds = ["3", "394"]

    @State private var index = 0
    @State private var currentExercise = "3"
    @State private var reps = 0
    @State private var isLoading = false

    var body: some View {
        VStack(spacing: 16) {
            Text("Reps: \(reps)")
                .font(.title)
                .padding(.top)

            kinestex.createCameraView(
                exercises: exerciseIds,
                currentExercise: $currentExercise,
                user: nil,
                isLoading: $isLoading,
                onMessageReceived: { message in
                    if case .reps(let value) = message {
                        reps = value["value"] as? Int ?? 0
                    }
                }
            )

            HStack(spacing: 24) {
                Button("Previous") { switchTo(index - 1) }
                Button("Next")     { switchTo(index + 1) }
            }
            .padding(.bottom)
        }
    }

    private func switchTo(_ newIndex: Int) {
        index = (newIndex + exerciseIds.count) % exerciseIds.count
        currentExercise = exerciseIds[index]
        reps = 0
    }
}
```

_Kotlin (Android)_
```kotlin
import android.Manifest
import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow

class CameraActivity : AppCompatActivity(), PermissionHandler {
    // 3 = Squats, 394 = Jumping Jack
    private val exerciseIds = listOf("3", "394")
    private var index = 0
    private val isLoading = MutableStateFlow(false)
    private lateinit var camera: GenericWebView
    private lateinit var tvReps: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)

        tvReps = findViewById(R.id.tvReps)

        camera = KinesteXSDK.createCameraComponent(
            context = this,
            currentExercise = exerciseIds[0],
            exercises = exerciseIds,
            user = null,
            isLoading = isLoading,
            onMessageReceived = { msg ->
                if (msg is WebViewMessage.Reps) {
                    val v = msg.data["value"] as? Int ?: 0
                    runOnUiThread { tvReps.text = "Reps: $v" }
                }
            },
            permissionHandler = this
        ) as GenericWebView
        findViewById<LinearLayout>(R.id.cameraContainer).addView(camera)

        findViewById<Button>(R.id.btnPrev).setOnClickListener { switchTo(index - 1) }
        findViewById<Button>(R.id.btnNext).setOnClickListener { switchTo(index + 1) }
    }

    private fun switchTo(newIndex: Int) {
        index = (newIndex + exerciseIds.size) % exerciseIds.size
        KinesteXSDK.updateCurrentExercise(exerciseIds[index])
        runOnUiThread { tvReps.text = "Reps: 0" }
    }

    override fun requestCameraPermission() {
        registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted -> camera.handlePermissionResult(granted) }
            .launch(Manifest.permission.CAMERA)
    }
}
```

_React Native_
```jsx
import { useRef, useState } from 'react';
import { View, Text, Button } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import {
  IntegrationOption,
  KinesteXSDKCamera,
  IPostData,
} from 'kinestex-sdk-react-native/src/types';

// 3 = Squats, 394 = Jumping Jack
const exerciseIds = ['3', '394'];

export default function CameraScreen() {
  const ref = useRef<KinesteXSDKCamera>(null);
  const [index, setIndex] = useState(0);
  const [reps, setReps] = useState(0);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    currentExercise: exerciseIds[0],
    exercises: exerciseIds,
    style: { style: 'dark' },
  };

  const switchTo = (newIndex: number) => {
    const wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
    setIndex(wrapped);
    setReps(0);
    ref.current?.changeExercise(exerciseIds[wrapped]);
  };

  return (
    <View style={{ flex: 1 }}>
      <Text style={{ fontSize: 24, padding: 16 }}>Reps: {reps}</Text>
      <View style={{ flex: 1 }}>
        <KinestexSDK
          ref={ref}
          data={postData}
          integrationOption={IntegrationOption.CAMERA}
          handleMessage={(type, data) => {
            if (type === 'successful_repeat') setReps(data.value);
          }}
        />
      </View>
      <View style={{ flexDirection: 'row', justifyContent: 'space-around', padding: 16 }}>
        <Button title="Previous" onPress={() => switchTo(index - 1)} />
        <Button title="Next"     onPress={() => switchTo(index + 1)} />
      </View>
    </View>
  );
}
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';

class CameraScreen extends StatefulWidget {
  const CameraScreen({super.key});
  @override
  State<CameraScreen> createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen> {
  // 3 = Squats, 394 = Jumping Jack
  final exerciseIds = const ['3', '394'];

  int index = 0;
  int reps = 0;
  final showKinesteX = ValueNotifier<bool>(true);
  final updateExercise = ValueNotifier<String?>('3');

  void switchTo(int newIndex) {
    final wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
    setState(() {
      index = wrapped;
      reps = 0;
    });
    updateExercise.value = exerciseIds[wrapped];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text('Reps: $reps', style: const TextStyle(fontSize: 24)),
          ),
          Expanded(
            child: ValueListenableBuilder<String?>(
              valueListenable: updateExercise,
              builder: (context, value, _) {
                return KinesteXAIFramework.createCameraComponent(
                  isShowKinestex: showKinesteX,
                  exercises: exerciseIds,
                  currentExercise: value ?? exerciseIds[0],
                  updatedExercise: value,
                  isLoading: ValueNotifier<bool>(false),
                  onMessageReceived: (m) {
                    if (m is Reps) {
                      setState(() => reps = m.data['value'] ?? 0);
                    }
                  },
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                ElevatedButton(
                  onPressed: () => switchTo(index - 1),
                  child: const Text('Previous'),
                ),
                ElevatedButton(
                  onPressed: () => switchTo(index + 1),
                  child: const Text('Next'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
```

_HTML / JavaScript_
```html
<!doctype html>
<html>
  <body style="margin:0;display:flex;flex-direction:column;height:100vh;">
    <h2 id="reps" style="padding:16px;margin:0;">Reps: 0</h2>
    <iframe
      id="camera"
      src="https://ai.kinestex.com/camera"
      style="flex:1;border:0;"
      allow="camera"
    ></iframe>
    <div style="display:flex;justify-content:space-around;padding:16px;">
      <button id="prev">Previous</button>
      <button id="next">Next</button>
    </div>

    <script>
      // 3 = Squats, 394 = Jumping Jack
      const exerciseIds = ["3", "394"];
      let index = 0;

      const camera = document.getElementById("camera");
      const repsEl = document.getElementById("reps");
      const srcURL = "https://ai.kinestex.com/camera";

      const postData = {
        key: "YOUR_API_KEY",
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY_NAME",
        currentExercise: exerciseIds[0],
        exercises: exerciseIds,
      };

      camera.onload = () => camera.contentWindow.postMessage(postData, srcURL);

      window.addEventListener("message", (event) => {
        if (event.origin !== "https://ai.kinestex.com") return;
        const msg = JSON.parse(event.data);
        if (msg.type === "successful_repeat") {
          repsEl.textContent = "Reps: " + msg.value;
        }
      });

      function switchTo(newIndex) {
        index = (newIndex + exerciseIds.length) % exerciseIds.length;
        repsEl.textContent = "Reps: 0";
        camera.contentWindow.postMessage(
          { currentExercise: exerciseIds[index] },
          srcURL
        );
      }

      document.getElementById("prev").onclick = () => switchTo(index - 1);
      document.getElementById("next").onclick = () => switchTo(index + 1);
    </script>
  </body>
</html>
```

_React (TypeScript)_
```tsx
import { useRef, useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

// 3 = Squats, 394 = Jumping Jack
const exerciseIds = ['3', '394'];

export default function CameraScreen() {
  const ref = useRef<KinesteXSDKCamera>(null);
  const [index, setIndex] = useState(0);
  const [reps, setReps] = useState(0);

  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    userId: 'YOUR_USER_ID',
    company: 'YOUR_COMPANY_NAME',
    currentExercise: exerciseIds[0],
    exercises: exerciseIds,
    style: { style: 'dark' },
  };

  const switchTo = (newIndex: number) => {
    const wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
    setIndex(wrapped);
    setReps(0);
    ref.current?.changeExercise(exerciseIds[wrapped]);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <h2 style={{ padding: 16, margin: 0 }}>Reps: {reps}</h2>
      <div style={{ flex: 1 }}>
        <KinesteXSDK
          ref={ref}
          data={postData}
          integrationOption={IntegrationOption.CAMERA}
          handleMessage={(type, data) => {
            if (type === 'successful_repeat') setReps(data.value as number);
          }}
        />
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-around', padding: 16 }}>
        <button onClick={() => switchTo(index - 1)}>Previous</button>
        <button onClick={() => switchTo(index + 1)}>Next</button>
      </div>
    </div>
  );
}
```

---
Source: https://www.kinestex.com/docs/integration · Index: https://www.kinestex.com/llms.txt
