import { useMemo } from 'react';
import { Box } from '@chakra-ui/react';
import { CodeTab, PreviewTab, TabsLayout } from '../../components/common/TabsLayout';
import CodeExample from '../../components/code/CodeExample';
import Customize from '../../components/common/Preview/Customize';
import Dependencies from '../../components/code/Dependencies';
import PreviewColorPickerCustom from '../../components/common/Preview/PreviewColorPickerCustom';
import PreviewInput from '../../components/common/Preview/PreviewInput';
import PreviewSelect from '../../components/common/Preview/PreviewSelect';
import PreviewSlider from '../../components/common/Preview/PreviewSlider';
import PreviewSwitch from '../../components/common/Preview/PreviewSwitch';
import PropTable from '../../components/common/Preview/PropTable';
import useComponentProps from '../../hooks/useComponentProps';
import { ComponentPropsProvider } from '../../components/context/ComponentPropsContext';
import { useColorModeValue } from '../../components/setup/color-mode';
import TextLoop from '../../content/TextAnimations/TextLoop/TextLoop';
import { textLoop } from '../../constants/code/TextAnimations/textLoopCode';
const LOOK = {
textColor: '',
ribbonColor: '#5227FF',
crossInk: '',
shape: 'wave',
speed: 90,
scrollBoost: 0,
draggable: false,
curviness: 90,
tilt: 0,
cross: false,
crossAngle: 16,
ribbon: true,
ribbonWidth: 86,
edgeFade: 0,
fontSize: 46,
fontWeight: 800,
letterSpacing: 2,
uppercase: true,
outline: false,
outlineWidth: 1.5
};
const PRESETS = {
ribbon: { ...LOOK },
tape: {
...LOOK,
textColor: '#120f17',
ribbonColor: '#ff5a36',
shape: 'line',
speed: 120,
draggable: true,
cross: true,
crossAngle: 14,
ribbonWidth: 76,
fontSize: 40,
letterSpacing: 1
},
orbit: {
...LOOK,
shape: 'circle',
speed: 60,
draggable: true,
curviness: 120,
cross: true,
crossAngle: 30,
ribbon: false,
fontSize: 32,
fontWeight: 600,
letterSpacing: 1,
uppercase: false
},
drift: {
...LOOK,
speed: 70,
scrollBoost: 1.5,
curviness: 130,
ribbon: false,
edgeFade: 0.8,
fontSize: 64,
fontWeight: 700,
letterSpacing: -1,
uppercase: false,
outline: true,
outlineWidth: 1.5
}
};
const PRESET_OPTIONS = [
{ value: 'ribbon', label: 'Ribbon' },
{ value: 'tape', label: 'Tape' },
{ value: 'orbit', label: 'Orbit' },
{ value: 'drift', label: 'Drift' }
];
const DEFAULT_PROPS = {
preset: 'ribbon',
text: 'React ✦ Bits',
separator: '✦',
direction: 'forward',
...LOOK,
pauseOnHover: true
};
const SHAPE_OPTIONS = [
{ value: 'wave', label: 'Wave' },
{ value: 'circle', label: 'Circle' },
{ value: 'infinity', label: 'Infinity' },
{ value: 'arch', label: 'Arch' },
{ value: 'line', label: 'Line' }
];
const DIRECTION_OPTIONS = [
{ value: 'forward', label: 'Forward' },
{ value: 'reverse', label: 'Reverse' }
];
const tint = (color, amount) => {
const match = /^#?([\da-f]{3}|[\da-f]{6})$/i.exec(String(color || '').trim());
if (!match) return color;
const hex = match[1].length === 3 ? match[1].replace(/./g, ch => ch + ch) : match[1];
return `#${[0, 2, 4]
.map(i => parseInt(hex.slice(i, i + 2), 16))
.map(v =>
Math.round(v + (255 - v) * amount)
.toString(16)
.padStart(2, '0')
)
.join('')}`;
};
const propData = [
{ name: 'text', type: 'string', default: '"React ✦ Bits"', description: 'The phrase repeated along the curve.' },
{
name: 'shape',
type: '"wave" | "circle" | "infinity" | "arch" | "line"',
default: '"wave"',
description: 'Built-in curve the text flows along.'
},
{
name: 'path',
type: 'string',
default: 'undefined',
description: 'Custom SVG path data, drawn in a 1200x520 viewBox. Overrides shape when provided.'
},
{
name: 'speed',
type: 'number',
default: '90',
description: 'Travel speed along the path, in units per second.'
},
{
name: 'direction',
type: '"forward" | "reverse"',
default: '"forward"',
description: 'Direction the text scrolls around the curve.'
},
{ name: 'separator', type: 'string', default: '"✦"', description: 'Glyph placed between each repetition.' },
{
name: 'curviness',
type: 'number',
default: '90',
description: 'Amplitude of the wave, or the radius of the closed shapes.'
},
{
name: 'tilt',
type: 'number',
default: '0',
description: 'Rotates the whole loop, in degrees. A slight tilt on a line turns it into a slanted banner.'
},
{
name: 'cross',
type: 'boolean',
default: 'false',
description:
'Adds a second band crossing the first in an X and running the other way, with a soft shadow where the top band passes over it. Waves weave into a braid, arches into a lens and circles gain a tilted orbit.'
},
{
name: 'crossColor',
type: 'string',
default: '-',
description:
'Color of the crossing band. Defaults to a pale tint of ribbonColor, and its text picks black or white for contrast. Without a ribbon it colors the crossing text, which is dimmed by default.'
},
{
name: 'crossAngle',
type: 'number',
default: '16',
description: 'Angle between the two bands, in degrees. The pair stays centered on tilt.'
},
{
name: 'draggable',
type: 'boolean',
default: 'false',
description:
'Drag the loop to scrub it, or spin it on a circle. A throw keeps its momentum, then eases back to the normal speed.'
},
{
name: 'scrollBoost',
type: 'number',
default: '0',
description:
'How strongly page scrolling pushes the loop. Scrolling down speeds it up, scrolling up slows or reverses it, and it eases back once scrolling stops. 0 turns it off.'
},
{ name: 'fontSize', type: 'number', default: '46', description: 'Font size of the looping text.' },
{ name: 'fontWeight', type: 'number', default: '800', description: 'Font weight of the looping text.' },
{ name: 'letterSpacing', type: 'number', default: '2', description: 'Extra tracking between letters.' },
{ name: 'uppercase', type: 'boolean', default: 'true', description: 'Renders the phrase in uppercase.' },
{
name: 'outline',
type: 'boolean',
default: 'false',
description: 'Draws the letters as outlines instead of solid fills.'
},
{ name: 'outlineWidth', type: 'number', default: '1.5', description: 'Stroke width of the outlined letters.' },
{
name: 'color',
type: 'string',
default: '-',
description: 'Fill color of the text. Defaults to white on the ribbon and to the surrounding text color without it.'
},
{
name: 'ribbon',
type: 'boolean',
default: 'true',
description: 'Draws a solid band behind the text along the path.'
},
{ name: 'ribbonColor', type: 'string', default: '"#5227FF"', description: 'Color of the band behind the text.' },
{ name: 'ribbonWidth', type: 'number', default: '86', description: 'Thickness of the band behind the text.' },
{
name: 'edgeFade',
type: 'number',
default: '0',
description: 'Fades the loop out toward the left and right edges, from 0 (hard edges) to 1 (wide, soft fade).'
},
{
name: 'pauseOnHover',
type: 'boolean',
default: 'true',
description: 'Eases the loop to a stop while the pointer is over it, and back up to speed when it leaves.'
},
{ name: 'className', type: 'string', default: '""', description: 'Additional CSS classes for the wrapper.' },
{ name: 'style', type: 'object', default: '{}', description: 'Inline styles for the wrapper.' }
];
const TextLoopDemo = () => {
const { props, defaultProps, updateProp, updateProps, resetProps, hasChanges } = useComponentProps(DEFAULT_PROPS);
const { preset, textColor, crossInk, ...settings } = props;
const ink = useColorModeValue('#18181b', '#ffffff');
const applyPreset = value => {
const base = Object.fromEntries(Object.keys(LOOK).map(name => [name, defaultProps[name]]));
updateProps({ ...base, ...PRESETS[value], preset: value });
};
const computedProps = useMemo(
() => ({
...(textColor ? { color: textColor } : {}),
...(crossInk ? { crossColor: crossInk } : {})
}),
[textColor, crossInk]
);
return (
<ComponentPropsProvider
props={props}
defaultProps={DEFAULT_PROPS}
resetProps={resetProps}
hasChanges={hasChanges}
demoOnlyProps={['preset', 'textColor', 'crossInk']}
computedProps={computedProps}
>
<TabsLayout>
<PreviewTab>
<Box
className="demo-container"
p={0}
minH={430}
overflow="hidden"
position="relative"
display="flex"
alignItems="center"
justifyContent="center"
>
<TextLoop {...settings} color={textColor || undefined} crossColor={crossInk || undefined} />
</Box>
<Customize>
<PreviewSelect title="Preset" options={PRESET_OPTIONS} value={preset} onChange={applyPreset} />
<PreviewInput
title="Text"
value={settings.text}
placeholder="Your phrase"
maxLength={30}
onChange={value => updateProp('text', value)}
/>
<PreviewInput
title="Separator"
value={settings.separator}
placeholder="✦"
width={90}
maxLength={3}
onChange={value => updateProp('separator', value)}
/>
<PreviewColorPickerCustom
title="Text Color"
color={textColor || (settings.ribbon ? '#ffffff' : ink)}
onChange={value => updateProp('textColor', value)}
/>
<PreviewColorPickerCustom
title="Ribbon Color"
color={settings.ribbonColor}
onChange={value => updateProp('ribbonColor', value)}
/>
<PreviewColorPickerCustom
title="Cross Color"
color={crossInk || (settings.ribbon ? tint(settings.ribbonColor, 0.62) : textColor || ink)}
onChange={value => updateProp('crossInk', value)}
/>
<PreviewSelect
title="Shape"
options={SHAPE_OPTIONS}
value={settings.shape}
onChange={value => updateProp('shape', value)}
/>
<PreviewSelect
title="Direction"
options={DIRECTION_OPTIONS}
value={settings.direction}
onChange={value => updateProp('direction', value)}
/>
<PreviewSlider
title="Speed"
min={10}
max={260}
step={5}
value={settings.speed}
onChange={value => updateProp('speed', value)}
/>
<PreviewSlider
title="Scroll Boost"
min={0}
max={3}
step={0.1}
value={settings.scrollBoost}
onChange={value => updateProp('scrollBoost', value)}
/>
<PreviewSwitch
title="Draggable"
isChecked={settings.draggable}
onChange={value => updateProp('draggable', value)}
/>
<PreviewSwitch
title="Pause On Hover"
isChecked={settings.pauseOnHover}
onChange={value => updateProp('pauseOnHover', value)}
/>
<PreviewSlider
title="Curviness"
min={0}
max={160}
step={2}
value={settings.curviness}
onChange={value => updateProp('curviness', value)}
/>
<PreviewSlider
title="Tilt"
min={-20}
max={20}
step={1}
value={settings.tilt}
valueUnit="°"
onChange={value => updateProp('tilt', value)}
/>
<PreviewSwitch title="Cross" isChecked={settings.cross} onChange={value => updateProp('cross', value)} />
<PreviewSlider
title="Cross Angle"
min={0}
max={40}
step={1}
value={settings.crossAngle}
valueUnit="°"
isDisabled={!settings.cross}
onChange={value => updateProp('crossAngle', value)}
/>
<PreviewSwitch title="Ribbon" isChecked={settings.ribbon} onChange={value => updateProp('ribbon', value)} />
<PreviewSlider
title="Ribbon Width"
min={0}
max={160}
step={2}
value={settings.ribbonWidth}
valueUnit="px"
isDisabled={!settings.ribbon}
onChange={value => updateProp('ribbonWidth', value)}
/>
<PreviewSlider
title="Edge Fade"
min={0}
max={1}
step={0.05}
value={settings.edgeFade}
onChange={value => updateProp('edgeFade', value)}
/>
<PreviewSlider
title="Font Size"
min={18}
max={90}
step={2}
value={settings.fontSize}
valueUnit="px"
onChange={value => updateProp('fontSize', value)}
/>
<PreviewSlider
title="Font Weight"
min={300}
max={900}
step={50}
value={settings.fontWeight}
onChange={value => updateProp('fontWeight', value)}
/>
<PreviewSlider
title="Letter Spacing"
min={-2}
max={14}
step={0.5}
value={settings.letterSpacing}
valueUnit="px"
onChange={value => updateProp('letterSpacing', value)}
/>
<PreviewSwitch
title="Uppercase"
isChecked={settings.uppercase}
onChange={value => updateProp('uppercase', value)}
/>
<PreviewSwitch
title="Outline"
isChecked={settings.outline}
onChange={value => updateProp('outline', value)}
/>
<PreviewSlider
title="Outline Width"
min={0.5}
max={4}
step={0.1}
value={settings.outlineWidth}
valueUnit="px"
isDisabled={!settings.outline}
onChange={value => updateProp('outlineWidth', value)}
/>
</Customize>
<PropTable data={propData} />
<Dependencies dependencyList={['gsap']} />
</PreviewTab>
<CodeTab>
<CodeExample codeObject={textLoop} componentName="TextLoop" />
</CodeTab>
</TabsLayout>
</ComponentPropsProvider>
);
};
export default TextLoopDemo;