import { tokens } from '../core/tokens';import type { TabBarBehaviorProps } from '../core/types';import { useEffect, useState } from 'react';import { I18nManager, type LayoutChangeEvent } from 'react-native';import Animated, { Easing, useAnimatedProps, useReducedMotion, useSharedValue, withSpring, withTiming,} from 'react-native-reanimated';import { Circle, Path } from 'react-native-svg';export const AnimatedPath = Animated.createAnimatedComponent(Path);export const AnimatedCircle = Animated.createAnimatedComponent(Circle);type Options = Pick<TabBarBehaviorProps, 'items' | 'activeKey' | 'animation' | 'reducedMotion'>;const half = tokens.size.notchWidth / 2;const depth = tokens.size.notchDepth;const radius = tokens.radius.card;// Half a pixel in, so the 1px outline isn't clipped at the bar's edges.const inset = 0.5;/** * The bar outline with a notch in its top edge. The notch's floor runs from `a` to `b` (two tab centers), so while * they differ it stretches like a drop; when they meet it's a plain dip. */function cutoutPath(width: number, height: number, a: number, b: number, d: number): string { 'worklet'; const left = inset; const top = inset; const right = width - inset; const bottom = height - inset; const r = Math.min(radius, (bottom - top) / 2); const from = Math.max(left + r, Math.min(a, b) - half); const to = Math.min(right - r, Math.max(a, b) + half); const lo = Math.min(a, b); const hi = Math.max(a, b); return [ `M ${left + r} ${top}`, `L ${from} ${top}`, // Flat shoulder into a round floor: the curve leaves the edge level, so the cut looks soft, not chiselled. `C ${from + half * 0.6} ${top} ${lo - half * 0.45} ${top + d} ${lo} ${top + d}`, `L ${hi} ${top + d}`, `C ${hi + half * 0.45} ${top + d} ${to - half * 0.6} ${top} ${to} ${top}`, `L ${right - r} ${top}`, `A ${r} ${r} 0 0 1 ${right} ${top + r}`, `L ${right} ${bottom - r}`, `A ${r} ${r} 0 0 1 ${right - r} ${bottom}`, `L ${left + r} ${bottom}`, `A ${r} ${r} 0 0 1 ${left} ${bottom - r}`, `L ${left} ${top + r}`, `A ${r} ${r} 0 0 1 ${left + r} ${top}`, 'Z', ].join(' ');}/** * The notch and dot motion, shared by both versions of the bar. * * Two shared values hold the active index: `lead` eases out like the Glide Tab Bar and carries the dot; `trail` * follows on a softer spring. The notch spans both, so a tap pulls it into a drop that catches up and settles. The * path is rebuilt in a worklet on the UI thread; React only re-renders when a layout size changes. */export function useCutoutNotch({ items, activeKey, animation, reducedMotion = 'system' }: Options) { const systemReduced = useReducedMotion(); const reduce = reducedMotion === 'always' || (reducedMotion === 'system' && systemReduced); const count = items.length; const activeIndex = items.findIndex((item) => item.key === activeKey); const found = activeIndex >= 0; const rtl = I18nManager.isRTL; const [size, setSize] = useState({ width: 0, height: 0 }); const barWidth = useSharedValue(0); const barHeight = useSharedValue(0); const rowX = useSharedValue(0); const rowWidth = useSharedValue(0); const lead = useSharedValue(Math.max(activeIndex, 0)); const trail = useSharedValue(Math.max(activeIndex, 0)); const type = animation?.type ?? 'timing'; const durationMs = animation?.durationMs ?? tokens.motion.durationMs; const damping = animation?.spring?.damping ?? tokens.motion.spring.damping; const stiffness = animation?.spring?.stiffness ?? tokens.motion.spring.stiffness; const mass = animation?.spring?.mass ?? tokens.motion.spring.mass; useEffect(() => { if (!found) return; if (reduce) { lead.value = activeIndex; trail.value = activeIndex; return; } lead.value = type === 'spring' ? withSpring(activeIndex, { damping, stiffness, mass }) : withTiming(activeIndex, { duration: durationMs, easing: Easing.out(Easing.cubic) }); // The trailing edge is the goo: a looser spring, so it lags, stretches the notch, then snaps in. trail.value = withSpring(activeIndex, { damping, stiffness: stiffness * 0.6, mass }); }, [activeIndex, found, reduce, type, durationMs, damping, stiffness, mass, lead, trail]); const centerOf = (index: number) => { 'worklet'; const slot = count > 0 ? rowWidth.value / count : 0; const offset = (index + 0.5) * slot; return rowX.value + (rtl ? rowWidth.value - offset : offset); }; const pathProps = useAnimatedProps(() => { if (barWidth.value === 0) return { d: '' }; const slot = count > 0 ? rowWidth.value / count : 1; const a = found ? centerOf(lead.value) : -half * 4; const b = found ? centerOf(trail.value) : -half * 4; // A stretched notch gets a little shallower, like a drop thinning out. const stretch = Math.min(1, Math.abs(a - b) / slot); return { d: cutoutPath(barWidth.value, barHeight.value, a, b, depth * (1 - 0.3 * stretch)) }; }); const dotProps = useAnimatedProps(() => ({ cx: centerOf(lead.value), cy: inset + depth * 0.4, opacity: found && rowWidth.value > 0 ? 1 : 0, })); const onBarLayout = (event: LayoutChangeEvent) => { const { width, height } = event.nativeEvent.layout; barWidth.value = width; barHeight.value = height; setSize((current) => (current.width === width && current.height === height ? current : { width, height })); }; const onRowLayout = (event: LayoutChangeEvent) => { rowX.value = event.nativeEvent.layout.x; rowWidth.value = event.nativeEvent.layout.width; }; return { size, pathProps, dotProps, onBarLayout, onRowLayout };}