If you want to use a ListView, and your items are of fixed with, you can use an implementation of ScrollPhysics based on PageScrollPhysics used by the PageView.
This has the limitation that it only works for equally sized children.
import 'package:flutter/material.dart';
class SnapScrollPhysics extends ScrollPhysics {
  const SnapScrollPhysics({super.parent, required this.snapSize});
  final double snapSize;
  @override
  SnapScrollSize applyTo(ScrollPhysics? ancestor) {
    return SnapScrollSize(parent: buildParent(ancestor), snapSize: snapSize);
  }
  double _getPage(ScrollMetrics position) {
    return position.pixels / snapSize;
  }
  double _getPixels(ScrollMetrics position, double page) {
    return page * snapSize;
  }
  double _getTargetPixels(
      ScrollMetrics position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.5;
    } else if (velocity > tolerance.velocity) {
      page += 0.5;
    }
    return _getPixels(position, page.roundToDouble());
  }
  @override
  Simulation? createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent
    // ballistics, which should put us back in range at a page boundary.
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
      return super.createBallisticSimulation(position, velocity);
    }
    final Tolerance tolerance = this.tolerance;
    final double target = _getTargetPixels(position, tolerance, velocity);
    if (target != position.pixels) {
      return ScrollSpringSimulation(spring, position.pixels, target, velocity,
          tolerance: tolerance);
    }
    return null;
  }
  @override
  bool get allowImplicitScrolling => false;
}
You can see it in action here:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView(
        physics: SnapScrollPhysics(snapSize: MediaQuery.of(context).size.width/3),
        scrollDirection: Axis.horizontal,
        children: <Widget>[
          Container(
            width: MediaQuery.of(context).size.width/3,
            color: Colors.amber[900],
            child: const Center(child: Text('Entry A')),
          ),
          Container(
            width: MediaQuery.of(context).size.width/3,
            color: Colors.amber[800],
            child: const Center(child: Text('Entry B')),
          ),
          Container(
            width: MediaQuery.of(context).size.width/3,
            color: Colors.amber[700],
            child: const Center(child: Text('Entry C')),
          ),
          Container(
            width: MediaQuery.of(context).size.width/3,
            color: Colors.amber[600],
            child: const Center(child: Text('Entry D')),
          ),
          Container(
            width: MediaQuery.of(context).size.width/3,
            color: Colors.amber[500],
            child: const Center(child: Text('Entry E')),
          ),
          Container(
            width: MediaQuery.of(context).size.width/3,
            color: Colors.amber[400],
            child: const Center(child: Text('Entry F')),
          ),
          Container(
            width: MediaQuery.of(context).size.width/3,
            color: Colors.amber[300],
            child: const Center(child: Text('Entry G')),
          ),
        ],
      ),
    );
  }
}
class SnapScrollSize extends ScrollPhysics {
  const SnapScrollSize({super.parent, required this.snapSize});
  final double snapSize;
  @override
  SnapScrollSize applyTo(ScrollPhysics? ancestor) {
    return SnapScrollSize(parent: buildParent(ancestor), snapSize: snapSize);
  }
  double _getPage(ScrollMetrics position) {
    return position.pixels / snapSize;
  }
  double _getPixels(ScrollMetrics position, double page) {
    return page * snapSize;
  }
  double _getTargetPixels(
      ScrollMetrics position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.5;
    } else if (velocity > tolerance.velocity) {
      page += 0.5;
    }
    return _getPixels(position, page.roundToDouble());
  }
  @override
  Simulation? createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent
    // ballistics, which should put us back in range at a page boundary.
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
      return super.createBallisticSimulation(position, velocity);
    }
    final Tolerance tolerance = this.tolerance;
    final double target = _getTargetPixels(position, tolerance, velocity);
    if (target != position.pixels) {
      return ScrollSpringSimulation(spring, position.pixels, target, velocity,
          tolerance: tolerance);
    }
    return null;
  }
  @override
  bool get allowImplicitScrolling => false;
}