diff --git a/demo/backend/_includes/icons/back-white.svg b/demo/backend/_includes/icons/back-white.svg
new file mode 100644
index 000000000..083e0ef9e
--- /dev/null
+++ b/demo/backend/_includes/icons/back-white.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/demo/backend/advanced/community/elements/scroll-transform/_styles.xml.njk b/demo/backend/advanced/community/elements/scroll-transform/_styles.xml.njk
new file mode 100644
index 000000000..b793c0f7e
--- /dev/null
+++ b/demo/backend/advanced/community/elements/scroll-transform/_styles.xml.njk
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/demo/backend/advanced/community/elements/scroll-transform/index.xml.njk b/demo/backend/advanced/community/elements/scroll-transform/index.xml.njk
new file mode 100644
index 000000000..c8641a8e1
--- /dev/null
+++ b/demo/backend/advanced/community/elements/scroll-transform/index.xml.njk
@@ -0,0 +1,169 @@
+---
+permalink: "/backend/advanced/community/scroll-transform.xml"
+tags: "Advanced/Community/Elements"
+hv_title: "Scroll Transform"
+---
+
+{% extends 'templates/base.xml.njk' %}
+
+{% block styles %}
+ {% include './_styles.xml.njk' %}
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+
+ {% include 'icons/back-white.svg' %}
+
+
+
+
+
+
+
+
+ {% include 'icons/back.svg' %}
+
+ {{ hv_title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ hv_title }}
+
+
+
+
+ Velit voluptas et alias atque provident sapiente consequuntur deserunt. Dolorem et
+ non error dolorem voluptate amet accusantium. Corporis rerum sed labore quae sed qui quis quasi. Illo pariatur sint qui.
+ Quasi quaerat id molestias. Necessitatibus et ipsa quia asperiores laborum neque. Quisquam dolorem consequatur illum. Ut
+ magni iusto explicabo blanditiis quasi laborum incidunt earum. Eius ut in rerum ipsam. Officiis dolores suscipit
+ consequatur placeat commodi eum. Vel possimus placeat aut eos tempore saepe. Esse assumenda eum illo sed aut earum quia
+ voluptatibus. Recusandae qui iusto corporis sed atque. Veniam et possimus praesentium. Cum molestiae non velit minus
+ voluptatibus quos illo sed. Et omnis ut soluta qui inventore molestias. Dolores voluptatem perspiciatis exercitationem
+ consectetur minus illum. Quos quaerat omnis aut eius eos dolores velit. Et quia vel ea unde eum repudiandae. Repellat et
+ ab sed.
+
+
+
+ Velit voluptas et alias atque provident sapiente consequuntur deserunt. Dolorem et
+ non error dolorem voluptate amet accusantium. Corporis rerum sed labore quae sed qui quis quasi. Illo pariatur sint qui.
+ Quasi quaerat id molestias. Necessitatibus et ipsa quia asperiores laborum neque. Quisquam dolorem consequatur illum. Ut
+ magni iusto explicabo blanditiis quasi laborum incidunt earum. Eius ut in rerum ipsam. Officiis dolores suscipit
+ consequatur placeat commodi eum. Vel possimus placeat aut eos tempore saepe. Esse assumenda eum illo sed aut earum quia
+ voluptatibus. Recusandae qui iusto corporis sed atque. Veniam et possimus praesentium. Cum molestiae non velit minus
+ voluptatibus quos illo sed. Et omnis ut soluta qui inventore molestias. Dolores voluptatem perspiciatis exercitationem
+ consectetur minus illum. Quos quaerat omnis aut eius eos dolores velit. Et quia vel ea unde eum repudiandae. Repellat et
+ ab sed.
+
+
+ Velit voluptas et alias atque provident sapiente consequuntur deserunt. Dolorem et
+ non error dolorem voluptate amet accusantium. Corporis rerum sed labore quae sed qui quis quasi. Illo pariatur sint qui.
+ Quasi quaerat id molestias. Necessitatibus et ipsa quia asperiores laborum neque. Quisquam dolorem consequatur illum. Ut
+ magni iusto explicabo blanditiis quasi laborum incidunt earum. Eius ut in rerum ipsam. Officiis dolores suscipit
+ consequatur placeat commodi eum. Vel possimus placeat aut eos tempore saepe. Esse assumenda eum illo sed aut earum quia
+ voluptatibus. Recusandae qui iusto corporis sed atque. Veniam et possimus praesentium. Cum molestiae non velit minus
+ voluptatibus quos illo sed. Et omnis ut soluta qui inventore molestias. Dolores voluptatem perspiciatis exercitationem
+ consectetur minus illum. Quos quaerat omnis aut eius eos dolores velit. Et quia vel ea unde eum repudiandae. Repellat et
+ ab sed.
+
+
+ Velit voluptas et alias atque provident sapiente consequuntur deserunt. Dolorem et
+ non error dolorem voluptate amet accusantium. Corporis rerum sed labore quae sed qui quis quasi. Illo pariatur sint qui.
+ Quasi quaerat id molestias. Necessitatibus et ipsa quia asperiores laborum neque. Quisquam dolorem consequatur illum. Ut
+ magni iusto explicabo blanditiis quasi laborum incidunt earum. Eius ut in rerum ipsam. Officiis dolores suscipit
+ consequatur placeat commodi eum. Vel possimus placeat aut eos tempore saepe. Esse assumenda eum illo sed aut earum quia
+ voluptatibus. Recusandae qui iusto corporis sed atque. Veniam et possimus praesentium. Cum molestiae non velit minus
+ voluptatibus quos illo sed. Et omnis ut soluta qui inventore molestias. Dolores voluptatem perspiciatis exercitationem
+ consectetur minus illum. Quos quaerat omnis aut eius eos dolores velit. Et quia vel ea unde eum repudiandae. Repellat et
+ ab sed.
+
+
+{% endblock %}
diff --git a/demo/schema/scroll-transform.xsd b/demo/schema/scroll-transform.xsd
new file mode 100644
index 000000000..2a809807b
--- /dev/null
+++ b/demo/schema/scroll-transform.xsd
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/src/Components/ScrollTransform/ScrollTransform.tsx b/demo/src/Components/ScrollTransform/ScrollTransform.tsx
new file mode 100644
index 000000000..04bbe378d
--- /dev/null
+++ b/demo/src/Components/ScrollTransform/ScrollTransform.tsx
@@ -0,0 +1,132 @@
+import React, { useEffect, useRef } from 'react';
+import {
+ calculateValue,
+ namespaceURI,
+ parseTransformElement,
+ type TransformConfig,
+} from './utils';
+import { createStyleProp, useScrollContext } from 'hyperview';
+import * as Render from 'hyperview/src/services/render';
+import { Animated } from 'react-native';
+import type { HvComponentProps } from 'hyperview';
+import { NODE_TYPE } from 'hyperview';
+
+
+interface AnimatedValues {
+ [key: string]: Animated.Value;
+}
+
+const ScrollTransformContainer = (props: HvComponentProps) => {
+ const style = createStyleProp(props.element, props.stylesheets, {
+ ...props.options,
+ styleAttr: 'style',
+ });
+
+ // Parse transformOrigin prop
+ const transformOriginValue = props.element.getAttribute('transform-origin');
+
+ // Parse transform elements from children
+ const transformConfigs: TransformConfig[] = [];
+ const transformElements = Array.from(props.element.childNodes).filter(
+ (child) =>
+ child.nodeType === NODE_TYPE.ELEMENT_NODE &&
+ (child as Element).namespaceURI === namespaceURI &&
+ (child as Element).localName === 'transform'
+ );
+
+ transformElements.forEach((element) => {
+ try {
+ const config = parseTransformElement(element as Element);
+ transformConfigs.push(config);
+ } catch (error) {
+ console.error('Error parsing transform element:', error);
+ }
+ });
+
+ // Create animated values for each transform
+ const animatedValuesRef = useRef({});
+ const { offsets } = useScrollContext();
+ //console.log('offsets', offsets);
+ // Initialize animated values
+ transformConfigs.forEach((config, index) => {
+ const key = `${config.styleAttr || config.transformAttr}-${index}`;
+ if (!animatedValuesRef.current[key]) {
+ // Assumes the default scroll position is 0
+ const defaultPosition = { x: 0, y: 0 };
+ const contextPosition = offsets[config.contextKey] || defaultPosition;
+ const position = config.axis === 'horizontal' ? contextPosition.x : contextPosition.y;
+ const initialValue = calculateValue(position, config.scrollRange, config.attrRange);
+ animatedValuesRef.current[key] = new Animated.Value(initialValue);
+ }
+ });
+
+ // Update animated values when scroll position changes
+ useEffect(() => {
+ transformConfigs.forEach((config, index) => {
+ const key = `${config.styleAttr || config.transformAttr}-${index}`;
+ const defaultPosition = { x: 0, y: 0 };
+ const contextPosition = offsets[config.contextKey] || defaultPosition;
+ const position = config.axis === 'horizontal' ? contextPosition.x : contextPosition.y;
+ const toValue = calculateValue(position, config.scrollRange, config.attrRange);
+
+ Animated.timing(animatedValuesRef.current[key], {
+ duration: config.duration,
+ toValue,
+ useNativeDriver: true,
+ }).start();
+ });
+ }, [transformConfigs, offsets]);
+
+ // Build animated style from transform configs
+ const animatedStyle: any = {};
+ const transformArray: any[] = [];
+
+ transformConfigs.forEach((config, index) => {
+ const key = `${config.styleAttr || config.transformAttr}-${index}`;
+ const animatedValue = animatedValuesRef.current[key];
+
+ if (config.styleAttr) {
+ // For style attributes like opacity, backgroundColor, etc.
+ animatedStyle[config.styleAttr] = animatedValue;
+ } else if (config.transformAttr) {
+ // For transform attributes like scale, rotate, etc.
+ const transformObj: any = {};
+ transformObj[config.transformAttr] = animatedValue;
+ transformArray.push(transformObj);
+ }
+ });
+
+ if (transformArray.length > 0) {
+ animatedStyle.transform = transformArray;
+ }
+
+ // Apply transformOrigin if specified
+ if (transformOriginValue) {
+ animatedStyle.transformOrigin = transformOriginValue;
+ }
+
+ // Filter out transform child elements since they're configuration, not content
+ const nonTransformChildNodes = Array.from(props.element.childNodes).filter(
+ (child) => {
+ if (child.nodeType === NODE_TYPE.ELEMENT_NODE) { // Element node
+ const element = child as Element;
+ return !(element.namespaceURI === namespaceURI && element.localName === 'transform');
+ }
+ return true; // Keep text nodes and other types
+ }
+ );
+
+ const children = Render.renderChildNodes(
+ nonTransformChildNodes,
+ props.stylesheets,
+ props.onUpdate,
+ props.options,
+ );
+
+ return {children};
+};
+
+ScrollTransformContainer.namespaceURI = namespaceURI;
+ScrollTransformContainer.localName = 'container';
+
+export { ScrollTransformContainer };
diff --git a/demo/src/Components/ScrollTransform/index.ts b/demo/src/Components/ScrollTransform/index.ts
new file mode 100644
index 000000000..f599dbb58
--- /dev/null
+++ b/demo/src/Components/ScrollTransform/index.ts
@@ -0,0 +1 @@
+export { ScrollTransformContainer } from './ScrollTransform';
diff --git a/demo/src/Components/ScrollTransform/utils.ts b/demo/src/Components/ScrollTransform/utils.ts
new file mode 100644
index 000000000..ffe957312
--- /dev/null
+++ b/demo/src/Components/ScrollTransform/utils.ts
@@ -0,0 +1,115 @@
+export const namespaceURI = 'https://hyperview.org/scroll-transform';
+
+export const getNumberAttr = (
+ element: Element,
+ attrName: string,
+ defaultValue: number,
+): number => {
+ const value: string | null = element.getAttribute(attrName);
+ if (!value) {
+ return defaultValue;
+ }
+ return parseInt(value, 10);
+};
+
+export const getRangeAttr = (
+ element: Element,
+ attrName: string,
+ defaultValue: [number, number],
+): [number, number] => {
+ const value: string | null = element.getAttribute(attrName);
+ if (!value) {
+ return defaultValue;
+ }
+ try {
+ return JSON.parse(value);
+ } catch (e) {
+ throw new Error(`Invalid range attribute: ${value}`);
+ }
+};
+
+// Reuse the same calculation function as ScrollOpacity
+export const calculateValue = (
+ position: number,
+ inputRange: [number, number],
+ outputRange: [number, number],
+) => {
+ const [a, b] = inputRange;
+ const [c, d] = outputRange;
+
+ // Check if the input range has the same start and end values
+ if (a === b) {
+ // If the position is exactly at a (or b), return the start of the output range
+ // Otherwise, return the end of the output range
+ return position <= a ? c : d;
+ }
+
+ // Calculate the slope
+ const slope = (d - c) / (b - a);
+
+ // Handle both ascending and descending input ranges
+ const isAscending = b > a;
+
+ if (isAscending) {
+ // Ascending range: a < b
+ if (position < a) {
+ return c;
+ }
+ if (position > b) {
+ return d;
+ }
+ } else {
+ // Descending range: a > b
+ if (position > a) {
+ return c;
+ }
+ if (position < b) {
+ return d;
+ }
+ }
+
+ // Interpolate between the ranges
+ return c + slope * (position - a);
+};
+
+export interface TransformConfig {
+ contextKey: string;
+ styleAttr?: string;
+ transformAttr?: string;
+ scrollRange: [number, number];
+ attrRange: [number, number];
+ axis: 'horizontal' | 'vertical';
+ duration: number;
+}
+
+export const parseTransformElement = (element: Element): TransformConfig => {
+ const contextKey = element.getAttribute('context-key');
+ const styleAttr = element.getAttribute('style-attr') || undefined;
+ const transformAttr = element.getAttribute('transform-attr') || undefined;
+ const axis = (element.getAttribute('axis') || 'vertical') as 'horizontal' | 'vertical';
+ const scrollRange = getRangeAttr(element, 'scroll-range', [0, 100]);
+ const attrRange = getRangeAttr(element, 'attr-range', [0, 1]);
+ const duration = getNumberAttr(element, 'duration', 0);
+
+ if (!contextKey) {
+ throw new Error('context-key is required for scroll-transform:transform');
+ }
+
+ if (!styleAttr && !transformAttr) {
+ throw new Error('Either style-attr or transform-attr is required for scroll-transform:transform');
+ }
+
+ if (styleAttr && transformAttr) {
+ throw new Error('Cannot specify both style-attr and transform-attr for scroll-transform:transform');
+ }
+
+ return {
+ contextKey,
+ styleAttr,
+ transformAttr,
+ scrollRange,
+ attrRange,
+ axis,
+ duration,
+ };
+};
diff --git a/demo/src/Components/index.ts b/demo/src/Components/index.ts
index ec8785b97..0e9a0b6a2 100644
--- a/demo/src/Components/index.ts
+++ b/demo/src/Components/index.ts
@@ -6,6 +6,7 @@ import { NavBack } from './NavBack';
import { ProgressBar } from './ProgressBar';
import { SafeAreaView } from './SafeAreaView';
import { ScrollOpacity } from './ScrollOpacity';
+import { ScrollTransformContainer } from './ScrollTransform';
import { Svg } from './Svg';
export default [
@@ -19,5 +20,6 @@ export default [
ProgressBar,
SafeAreaView,
ScrollOpacity,
+ ScrollTransformContainer,
Svg,
];